Sign In

@saihm/mcp-server

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@saihm/mcp-server - npm Package Compare versions

Comparing version
0.3.9
to
0.3.10
+292
-55
dist/saihm_mcp_server.js

@@ -54,2 +54,40 @@ #!/usr/bin/env node

}
/**
* Read a numeric field off an operator response.
*
* The declared types describe what an operator SHOULD send; the wire decides what
* it actually sends, and the client casts the JSON without validating it. Present
* is therefore not the same as numeric: an operator that serialises numbers as
* strings is entirely normal here — `bfsi_R`, `bfsi_M` and `snapshotEpoch` are
* declared as strings for exactly that reason — and calling `.toFixed()` on one
* crashes the tool with `d.bfsi.toFixed is not a function`, which tells the user
* nothing. So accept a numeric string, and treat anything else (including `NaN`
* and `Infinity`, which would print as fact) as not reported.
*/
function asNumber(v) {
if (typeof v === 'number')
return Number.isFinite(v) ? v : undefined;
if (typeof v === 'string' && v.trim() !== '') {
const n = Number(v);
return Number.isFinite(n) ? n : undefined;
}
return undefined;
}
/**
* Read a field that is printed rather than computed with.
*
* Same reasoning as {@link asNumber}, for the other half of the response: an object
* interpolates as `[object Object]` and an array as its comma-joined contents, both
* of which read as real values. Anything that is not a primitive is treated as not
* reported, so it is left out instead of printed as garbage.
*/
function asScalar(v) {
if (typeof v === 'string')
return v;
if (typeof v === 'number')
return Number.isFinite(v) ? String(v) : undefined;
if (typeof v === 'bigint' || typeof v === 'boolean')
return String(v);
return undefined;
}
server.registerTool('saihm_remember', {

@@ -59,10 +97,14 @@ title: 'Remember',

inputSchema: { content: z.string().describe('Information to remember') },
// The cell id is the whole receipt: it is what confirms the write and what
// saihm_forget needs later. The rest is detail an operator may or may not
// return, so requiring it here would not catch a thin receipt anyway —
// `String(undefined)` is the string "undefined", which satisfies z.string().
outputSchema: {
cellId: z.string(),
cellNonce: z.string(),
tier: z.string(),
kekVersion: z.string(),
epoch: z.string(),
feeNcoti: z.string(),
signaturePrefix: z.string(),
cellNonce: z.string().optional(),
tier: z.string().optional(),
kekVersion: z.string().optional(),
epoch: z.string().optional(),
feeNcoti: z.string().optional(),
signaturePrefix: z.string().optional(),
},

@@ -78,2 +120,32 @@ annotations: {

const r = await getRuntime().remember(content);
// A receipt with no cell id does not confirm a write: there is nothing to quote
// back and nothing to hand saihm_forget later. Reporting it as REMEMBERED would
// tell the user their memory is safe on the strength of an acknowledgement the
// operator never actually gave.
if (typeof r.cellId !== 'string' || r.cellId.length === 0)
throw new Error('The operator returned no cell id, so this write is unconfirmed and the' +
' memory could not be erased later even if it was stored. Treat it as not' +
' stored and report this to your operator.');
// Everything else is receipt detail. Render only what came back: a rendered
// `tier=undefined` reads to a user as a real value, and the output schema cannot
// catch it because `String(undefined)` is a perfectly valid string.
const nonce = asScalar(r.cellNonce);
const tier = asScalar(r.tier);
const kekVersion = asScalar(r.kekVersion);
const epoch = asScalar(r.epoch);
const feeNcoti = asScalar(r.feeNcoti);
const signaturePrefix = asScalar(r.signaturePrefix);
const detail = [];
if (nonce !== undefined)
detail.push(`nonce=${nonce}`);
if (tier !== undefined)
detail.push(`tier=${tier}`);
if (kekVersion !== undefined)
detail.push(`kekV=${kekVersion}`);
if (epoch !== undefined)
detail.push(`epoch=${epoch}`);
if (feeNcoti !== undefined)
detail.push(`fee=${feeNcoti}nCOTI`);
if (signaturePrefix !== undefined)
detail.push(`sig=${signaturePrefix}…`);
return {

@@ -83,3 +155,3 @@ content: [

type: 'text',
text: `REMEMBERED [${r.cellId}] nonce=${r.cellNonce} tier=${r.tier} kekV=${r.kekVersion} epoch=${r.epoch} fee=${r.feeNcoti}nCOTI sig=${r.signaturePrefix}…`,
text: `REMEMBERED [${r.cellId}]` + (detail.length > 0 ? ` ${detail.join(' ')}` : ''),
},

@@ -89,8 +161,8 @@ ],

cellId: r.cellId,
cellNonce: String(r.cellNonce),
tier: String(r.tier),
kekVersion: String(r.kekVersion),
epoch: String(r.epoch),
feeNcoti: String(r.feeNcoti),
signaturePrefix: String(r.signaturePrefix),
...(nonce !== undefined ? { cellNonce: nonce } : {}),
...(tier !== undefined ? { tier } : {}),
...(kekVersion !== undefined ? { kekVersion } : {}),
...(epoch !== undefined ? { epoch } : {}),
...(feeNcoti !== undefined ? { feeNcoti } : {}),
...(signaturePrefix !== undefined ? { signaturePrefix } : {}),
},

@@ -105,8 +177,11 @@ };

count: z.number(),
// The id and the plaintext are what a recall is for; the surrounding metadata
// is whatever the operator chose to send with it. See the handler: absent
// metadata is left out rather than rendered as the string "undefined".
memories: z.array(z.object({
cellId: z.string(),
kekVersion: z.string(),
cellNonce: z.string(),
timestamp: z.string(),
tier: z.string(),
kekVersion: z.string().optional(),
cellNonce: z.string().optional(),
timestamp: z.string().optional(),
tier: z.string().optional(),
plaintext: z.string(),

@@ -124,10 +199,42 @@ })),

const cells = await getRuntime().recall(query);
const memories = cells.map((c) => ({
cellId: c.cellId,
kekVersion: String(c.kekVersion),
cellNonce: String(c.cellNonce),
timestamp: String(c.timestamp),
tier: String(c.tier),
plaintext: c.plaintext,
}));
// The response is cast, not validated, so a non-list would reach `.filter` below
// and fail as `cells.filter is not a function` — a stack trace where a diagnosis
// belongs.
if (!Array.isArray(cells))
throw new Error('The operator returned a malformed recall response: expected a list of cells.' +
' Report this to your operator.');
// A non-custodial operator returns sealed cells and expects the CLIENT to open
// them. This package is deliberately crypto-free, so it holds no keys and there
// is no plaintext to return. Say so precisely and name the client that can — a
// permissive fallback here would report empty or sealed "memories" as if the
// recall had succeeded, which is worse than a clear refusal.
//
// Distinguish ALL-sealed from SOME-sealed: only the former is diagnostic of a
// non-custodial operator. A custodial operator that omits plaintext on part of a
// response (a tombstoned or unreadable cell, say) is a different fault, and
// blaming custody for it would send the user to the wrong fix.
const sealed = cells.filter((c) => typeof c.plaintext !== 'string');
if (sealed.length > 0 && sealed.length === cells.length)
throw new Error('This operator is non-custodial: it stores only ciphertext and this client' +
' holds no decryption keys, so it cannot read your memories. Use' +
' @saihm/mcp-server-pro, which seals and opens cells on your own machine:' +
' npx -y @saihm/mcp-server-pro free-join');
if (sealed.length > 0)
throw new Error(`Operator returned ${sealed.length} of ${cells.length} cells without plaintext` +
` (first: ${sealed[0].cellId}). This client cannot decrypt, so the recall is` +
' incomplete; report this to your operator.');
const memories = cells.map((c) => {
const kekVersion = asScalar(c.kekVersion);
const cellNonce = asScalar(c.cellNonce);
const timestamp = asScalar(c.timestamp);
const tier = asScalar(c.tier);
return {
cellId: c.cellId,
...(kekVersion !== undefined ? { kekVersion } : {}),
...(cellNonce !== undefined ? { cellNonce } : {}),
...(timestamp !== undefined ? { timestamp } : {}),
...(tier !== undefined ? { tier } : {}),
plaintext: c.plaintext,
};
});
if (cells.length === 0)

@@ -139,4 +246,18 @@ return {

const lines = [`RECALL ${cells.length} memories`];
for (const c of cells)
lines.push(` [${c.cellId}] kekV=${c.kekVersion} nonce=${c.cellNonce} ${c.timestamp} (${c.tier}) | ${c.plaintext}`);
for (const c of cells) {
const meta = [];
const kekVersion = asScalar(c.kekVersion);
const cellNonce = asScalar(c.cellNonce);
const timestamp = asScalar(c.timestamp);
const tier = asScalar(c.tier);
if (kekVersion !== undefined)
meta.push(`kekV=${kekVersion}`);
if (cellNonce !== undefined)
meta.push(`nonce=${cellNonce}`);
if (timestamp !== undefined)
meta.push(timestamp);
if (tier !== undefined)
meta.push(`(${tier})`);
lines.push(` [${c.cellId}]` + (meta.length > 0 ? ` ${meta.join(' ')}` : '') + ` | ${c.plaintext}`);
}
return {

@@ -175,14 +296,19 @@ content: [{ type: 'text', text: lines.join('\n') }],

title: 'Status',
description: 'Show SAIHM session status (PRS, BFSI, storage by tier, sharing, PHI). Use this to check the agent identity, reputation, storage, and sharing state of the current SAIHM session.',
description: 'Show SAIHM session status (PRS, BFSI, storage by tier, sharing, PHI), as far as the operator reports them — a non-custodial operator cannot see stored-byte totals. Use this to check the agent identity, reputation, storage, and sharing state of the current SAIHM session.',
inputSchema: {},
// Which of these an operator can answer depends on its custody model, so every
// field a non-custodial operator cannot see is optional. Only the agent identity
// — which every operator reports whatever its model — stays required. See the
// handler for why absence is not an error.
outputSchema: {
agentIdHash: z.string(),
prsScore: z.string(),
prsLevel: z.string(),
bfsiScore: z.number(),
feeDiscountPct: z.number(),
activeShardCount: z.number(),
activeSharingContracts: z.number(),
phi: z.number(),
snapshotEpoch: z.string(),
custody: z.string().optional(),
prsScore: z.string().optional(),
prsLevel: z.string().optional(),
bfsiScore: z.number().optional(),
feeDiscountPct: z.number().optional(),
activeShardCount: z.number().optional(),
activeSharingContracts: z.number().optional(),
phi: z.number().optional(),
snapshotEpoch: z.string().optional(),
},

@@ -197,23 +323,134 @@ annotations: {

}, async () => {
const d = await getRuntime().status();
const tiers = Object.entries(d.storageByTier)
.map(([t, b]) => `${t}=${b}B`)
.join(' ');
// StatusSnapshot describes a fully custodial operator. A non-custodial one holds
// ciphertext and no keys, so per-tier byte totals, staking, PHI and PRS do not
// exist on its side at all — their absence is the honest answer, not a fault.
// Read the response as partial so the compiler forces a check on every field
// rather than trusting the interface: the wire decides what is present.
const d = (await getRuntime().status());
// Identity is the one field every operator reports whatever its custody model,
// but reported is not the same as usable: `.slice` on a non-string crashed the
// tool on its very first line, before any of the checks below could run.
const agentId = typeof d.agentIdHashHex === 'string' ? d.agentIdHashHex : '';
const custody = asScalar(d.custody);
const lines = ['SAIHM Session'];
lines.push((agentId ? ` agent=${agentId.slice(0, 16)}…` : ' agent: not reported by this operator') +
(custody ? ` custody=${custody}` : ''));
// Read every numeric through asNumber: present is not the same as numeric, and a
// field that is present but unusable is not reported rather than crashed on.
const bfsiScore = asNumber(d.bfsiScore);
const bfsi = asNumber(d.bfsi);
const feeDiscountPct = asNumber(d.feeDiscountPct);
const shardCount = asNumber(d.activeShardCount);
const sharingCount = asNumber(d.activeSharingContracts);
const phi = asNumber(d.phi);
const prs = asNumber(d.prs);
const prsScore = asScalar(d.prsScore);
const prsLevel = asScalar(d.prsLevel);
const snapshotEpoch = asScalar(d.snapshotEpoch);
// PRS/BFSI/fee discount: report each only where the operator actually supplies it.
const rep = [];
if (prsScore !== undefined)
rep.push(`PRS=${prsScore} (${prsLevel ?? 'n/a'})`);
if (bfsiScore !== undefined)
rep.push(`BFSI=${bfsiScore.toFixed(3)}`);
else if (bfsi !== undefined)
rep.push(`BFSI=${bfsi.toFixed(3)}`);
if (feeDiscountPct !== undefined)
rep.push(`feeDiscount=${(feeDiscountPct * 100).toFixed(1)}%`);
if (rep.length > 0)
lines.push(` ${rep.join(' ')}`);
// Object.entries on a string enumerates its characters, so a storageByTier that
// arrives as anything but an object would print per-character garbage as storage.
const tiers = typeof d.storageByTier === 'object' && d.storageByTier !== null
? Object.entries(d.storageByTier)
.map(([t, b]) => `${t}=${b}B`)
.join(' ')
: '';
const shardBits = [];
if (shardCount !== undefined)
shardBits.push(`shards=${shardCount}`);
if (tiers)
shardBits.push(tiers);
if (shardBits.length > 0)
lines.push(` ${shardBits.join(' ')}`);
const staking = d.stakingPosition;
if (typeof staking === 'object' && staking !== null) {
const stakeBits = [];
const amount = asScalar(staking.amountNcoti);
const yieldNcoti = asScalar(staking.accruedYieldNcoti);
if (amount !== undefined)
stakeBits.push(`staking=${amount}nCOTI`);
if (yieldNcoti !== undefined)
stakeBits.push(`yield=${yieldNcoti}nCOTI`);
if (stakeBits.length > 0)
lines.push(` ${stakeBits.join(' ')}`);
}
const sessionBits = [];
if (sharingCount !== undefined)
sessionBits.push(`sharing=${sharingCount}`);
if (phi !== undefined)
sessionBits.push(`PHI=${phi.toFixed(3)}`);
if (snapshotEpoch !== undefined)
sessionBits.push(`epoch=${snapshotEpoch}`);
if (sessionBits.length > 0)
lines.push(` ${sessionBits.join(' ')}`);
// The §3.4 spec fields, assembled from only the parts the operator actually sent.
// `contracts=0` is a real answer when the operator returns an empty list and a
// fabrication when it returns nothing at all, so an absent array is left out
// rather than counted as zero. With every field present this renders exactly as
// it did before 0.3.10.
const spec = [];
if (prs !== undefined)
spec.push(`prs=${prs.toFixed(3)}`);
if (bfsi !== undefined)
spec.push(`bfsi=${bfsi.toFixed(3)}`);
const bfsiWindow = [];
const bfsiR = asScalar(d.bfsi_R);
const bfsiM = asScalar(d.bfsi_M);
const bfsiWin = asScalar(d.bfsi_window_start_ts);
if (bfsiR !== undefined)
bfsiWindow.push(`R=${bfsiR}`);
if (bfsiM !== undefined)
bfsiWindow.push(`M=${bfsiM}`);
if (bfsiWin !== undefined)
bfsiWindow.push(`win=${bfsiWin}`);
if (bfsiWindow.length > 0)
spec.push(`(${bfsiWindow.join(' ')})`);
// Array.isArray, not truthiness: a `contracts` that arrives as a number or an
// object has no length, and `contracts=undefined` is worse than saying nothing.
if (Array.isArray(d.contracts))
spec.push(`contracts=${d.contracts.length}`);
if (Array.isArray(d.governance))
spec.push(`governance=${d.governance.length}`);
if (spec.length > 0)
lines.push(` §3.4: ${spec.join(' ')}`);
// Name what this operator structurally cannot answer, so an absent PRS reads as a
// property of its custody model rather than as a client that failed to display it.
// PRS travels either as the `prsScore` operator extension or as the §3.4 `prs`
// field; claiming it is unreported while the §3.4 line shows it would contradict
// the line above, so both have to be absent before saying so.
if (prsScore === undefined && prs === undefined)
lines.push(' PRS: not reported by this operator');
if (d.custody === 'non-custodial')
lines.push(' This operator is non-custodial: it stores only ciphertext, so it cannot' +
' report stored-byte totals or read your memories. Use @saihm/mcp-server-pro' +
' to read memory held by a non-custodial operator.');
return {
content: [
{
type: 'text',
text: `SAIHM Session\n agent=${d.agentIdHashHex.slice(0, 16)}…\n PRS=${d.prsScore} (${d.prsLevel}) BFSI=${d.bfsiScore.toFixed(3)} feeDiscount=${(d.feeDiscountPct * 100).toFixed(1)}%\n shards=${d.activeShardCount} ${tiers}\n staking=${d.stakingPosition.amountNcoti}nCOTI yield=${d.stakingPosition.accruedYieldNcoti}nCOTI\n sharing=${d.activeSharingContracts} PHI=${d.phi.toFixed(3)} epoch=${d.snapshotEpoch}\n §3.4: prs=${d.prs.toFixed(3)} bfsi=${d.bfsi.toFixed(3)} (R=${d.bfsi_R} M=${d.bfsi_M} win=${d.bfsi_window_start_ts}) contracts=${d.contracts.length} governance=${d.governance.length}`,
},
],
content: [{ type: 'text', text: lines.join('\n') }],
structuredContent: {
agentIdHash: d.agentIdHashHex,
prsScore: String(d.prsScore),
prsLevel: String(d.prsLevel),
bfsiScore: d.bfsiScore,
feeDiscountPct: d.feeDiscountPct,
activeShardCount: Number(d.activeShardCount),
activeSharingContracts: Number(d.activeSharingContracts),
phi: d.phi,
snapshotEpoch: String(d.snapshotEpoch),
agentIdHash: agentId,
...(custody !== undefined ? { custody } : {}),
...(prsScore !== undefined ? { prsScore } : {}),
...(prsLevel !== undefined ? { prsLevel } : {}),
// The numeric fields go out as the values that were actually usable. Passing
// the raw field through would put a string or a NaN into a z.number() slot
// and turn a thin response into an output-validation error.
...(bfsiScore !== undefined ? { bfsiScore } : {}),
...(feeDiscountPct !== undefined ? { feeDiscountPct } : {}),
// Omit rather than default: a fabricated 0 reads as "you have no shards"
// and an empty epoch reads as fact. Absence is the truthful signal.
...(shardCount !== undefined ? { activeShardCount: shardCount } : {}),
...(sharingCount !== undefined ? { activeSharingContracts: sharingCount } : {}),
...(phi !== undefined ? { phi } : {}),
...(snapshotEpoch !== undefined ? { snapshotEpoch } : {}),
},

@@ -220,0 +457,0 @@ };

@@ -143,2 +143,10 @@ /**

agentIdHashHex: string;
/**
* The operator's custody model, when it declares one. `"non-custodial"` means
* the operator holds ciphertext and no keys — it therefore cannot report
* stored-byte totals or return plaintext, and the fields below that depend on
* reading cell contents will be absent. Optional: operators that predate this
* field, or that are custodial, simply omit it.
*/
custody?: string;
prsScore: number;

@@ -145,0 +153,0 @@ prsLevel: string;

+6
-3

@@ -32,5 +32,8 @@ /**

' non-custodial companion client, which seals on your own machine; see its' +
' README for the one-time setup. To use THIS client against an operator' +
' instead, set SAIHM_ENDPOINT_URL and SAIHM_AUTH_HEADER; endpoints are issued' +
' at https://saihm.coti.global. To evaluate the protocol offline first (no' +
' README for the one-time setup. That is also the client for the hosted SAIHM' +
' service at https://saihm.coti.global: it is non-custodial and stores only' +
' ciphertext, so this crypto-free client cannot read memory held there. To use' +
' THIS client, set SAIHM_ENDPOINT_URL and SAIHM_AUTH_HEADER for a custodial' +
' operator that performs cryptography server-side. To evaluate the protocol' +
' offline first (no' +
' account, about a minute), run the demos at https://citw2.github.io/saihm-demos/';

@@ -37,0 +40,0 @@ function assertEndpointUrl(endpoint) {

{
"name": "@saihm/mcp-server",
"version": "0.3.9",
"version": "0.3.10",
"mcpName": "io.github.SAIHM-Admin/saihm-mcp",

@@ -5,0 +5,0 @@ "description": "The open MCP client for SAIHM sovereign agent memory — eight tools (remember · recall · forget · share · govern) any MCP agent uses to reach a SAIHM operator endpoint. Implements the publicly documented SAIHM memory protocol (specified in an Internet-Draft; not an IETF standard). For production client-side post-quantum sealing, pair with @saihm/client-pro. Evaluate it offline first with the runnable cross-model demos — no account. Apache-2.0.",

@@ -93,12 +93,22 @@ # SAIHM MCP Server

> **Don't have an endpoint and token yet?** They're issued by a SAIHM *operator*.
> The quickest path is a **free trial** — sign in with GitHub, no card, for
> testing on real infrastructure (see [Free trial](#free-trial-sign-in-with-github)
> below). Otherwise **join the hosted SAIHM service** at
> <https://saihm.coti.global> (managed, non-custodial storage — see
> [Join SAIHM](#prefer-not-to-run-storage-yourself-join-saihm) below), or
> **run your own operator endpoint**. Until one is configured, the tools have
> nowhere to reach and will return an error.
> This package is deliberately **crypto-free**, so it needs a **custodial**
> operator — one that performs cryptography server-side and returns plaintext.
>
> **The hosted SAIHM service at <https://saihm.coti.global> is not one.** It is
> non-custodial by design: it stores only ciphertext and never holds your keys,
> so cells sealed there can only be opened by a client that holds them. To use
> the hosted service — including the **free trial** (sign in with GitHub, no
> card) — use
> **[`@saihm/mcp-server-pro`](https://www.npmjs.com/package/@saihm/mcp-server-pro)**,
> which seals and opens on your own machine. See
> [Free trial](#free-trial-sign-in-with-github) and
> [Join SAIHM](#prefer-not-to-run-storage-yourself-join-saihm) below.
>
> Use *this* package against a custodial operator you run or subscribe to.
> Until one is configured, the tools have nowhere to reach and will return an
> error.
- **`SAIHM_ENDPOINT_URL`** — the SAIHM operator endpoint. Operators publish
their endpoint URLs at <https://saihm.coti.global>.
- **`SAIHM_ENDPOINT_URL`** — the endpoint of the **custodial** SAIHM operator you
run or subscribe to. Not the hosted service at <https://saihm.coti.global>,
which is non-custodial — see the note above.
- **`SAIHM_AUTH_HEADER`** — the `Authorization` header value the operator

@@ -105,0 +115,0 @@ expects (typically a `Bearer <token>` issued to you after key-bound