@saihm/mcp-server
Advanced tools
+292
-55
@@ -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; |
@@ -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) { |
+1
-1
| { | ||
| "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.", |
+19
-9
@@ -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 |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
132924
11.77%2199
12.6%395
2.6%