@saihm/mcp-server-pro 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,6 +11,11 @@ Production thin-client for **SAIHM non-custodial memory**.
11
11
 
12
12
  > **Key loss is unrecoverable by design.** If you lose your master secret you lose your KEK, and no one — including SAIHM — can open your cells. Back it up securely.
13
13
 
14
+ ## See it run
15
+
16
+ - **Live cross-model demos** — offline, ~1 min each, no account: <https://citw2.github.io/saihm-demos/>. Ground a memory you own in Claude, GPT, DeepSeek, Qwen, Kimi, or GLM, then prove you can erase it. `demo-claude-code` runs a stdio MCP server exactly like this one for Claude Code and Cursor.
17
+ - **Token benchmark** — recalling a bounded set of memory cells instead of re-sending the transcript cut input tokens by **62.8%–85.9%** (up to ~86%) across a realistic multi-session task; open, offline, reproducible: <https://github.com/citw2/saihm-token-benchmark>.
18
+
14
19
  ## Install
15
20
 
16
21
  ```sh
@@ -32,10 +37,10 @@ Claude Code, …) at it — paste this **once**:
32
37
  "SAIHM_ENDPOINT_URL": "https://saihm.coti.global/mcp",
33
38
  "SAIHM_MASTER_SECRET_HEX": "<your 64+ hex master secret>",
34
39
  "SAIHM_TIER": "PRO",
35
- "SAIHM_PAYMENT_METHOD": "stripe"
36
- }
37
- }
38
- }
40
+ "SAIHM_PAYMENT_METHOD": "stripe",
41
+ },
42
+ },
43
+ },
39
44
  }
40
45
  ```
41
46
 
@@ -117,15 +122,15 @@ The derived `saihm.agentIdHash` is the `sub` the endpoint binds your tenant to
117
122
 
118
123
  ## Configuration
119
124
 
120
- | Env | Required | Meaning |
121
- | ------------------------- | -------- | --------------------------------------------------------------------------------- |
122
- | `SAIHM_ENDPOINT_URL` | yes | `https://…/mcp` (or `http://` only for `127.0.0.1`/`localhost`). |
123
- | `SAIHM_AUTH_HEADER` | no | `Bearer <JWT>`, used verbatim. **Omit to self-onboard** (recommended): the client mints + auto-refreshes its own short-lived JWT from the master secret, so you paste one config once and never re-paste a token. |
124
- | `SAIHM_PAYMENT_METHOD` | self-onboard only | Your entitlement rail (e.g. `stripe`). Required when `SAIHM_AUTH_HEADER` is unset; ignored otherwise. |
125
- | `SAIHM_MASTER_SECRET_HEX` | yes\* | ≥ 64 hex chars (≥ 32 bytes), high-entropy, client-held; never sent. \*Provide this **or** `SAIHM_MASTER_SECRET_FILE`. |
126
- | `SAIHM_MASTER_SECRET_FILE`| yes\* | Path to a **mode-600** file holding the hex master secret. Preferred for operators: keeps the root seed out of a synced/shared MCP config. Takes precedence over `SAIHM_MASTER_SECRET_HEX` when both are set. |
127
- | `SAIHM_TIER` | self-onboard only | Tier label baked into sealed metadata. Required when self-onboarding; otherwise optional — resolved via `status()` if unset. |
128
- | `SAIHM_SEQ_STATE_PATH` | no | Persists per-cell sequence high-water marks (mode 600) for cross-restart updates. |
125
+ | Env | Required | Meaning |
126
+ | -------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
127
+ | `SAIHM_ENDPOINT_URL` | yes | `https://…/mcp` (or `http://` only for `127.0.0.1`/`localhost`). |
128
+ | `SAIHM_AUTH_HEADER` | no | `Bearer <JWT>`, used verbatim. **Omit to self-onboard** (recommended): the client mints + auto-refreshes its own short-lived JWT from the master secret, so you paste one config once and never re-paste a token. |
129
+ | `SAIHM_PAYMENT_METHOD` | self-onboard only | Your entitlement rail (e.g. `stripe`). Required when `SAIHM_AUTH_HEADER` is unset; ignored otherwise. |
130
+ | `SAIHM_MASTER_SECRET_HEX` | yes\* | ≥ 64 hex chars (≥ 32 bytes), high-entropy, client-held; never sent. \*Provide this **or** `SAIHM_MASTER_SECRET_FILE`. |
131
+ | `SAIHM_MASTER_SECRET_FILE` | yes\* | Path to a **mode-600** file holding the hex master secret. Preferred for operators: keeps the root seed out of a synced/shared MCP config. Takes precedence over `SAIHM_MASTER_SECRET_HEX` when both are set. |
132
+ | `SAIHM_TIER` | self-onboard only | Tier label baked into sealed metadata. Required when self-onboarding; otherwise optional — resolved via `status()` if unset. |
133
+ | `SAIHM_SEQ_STATE_PATH` | no | Persists per-cell sequence high-water marks (mode 600) for cross-restart updates. |
129
134
 
130
135
  > **Self-onboarding (paste once):** with `SAIHM_AUTH_HEADER` unset, the client proves
131
136
  > control of your identity via the endpoint's ML-DSA challenge/response and mints its own
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
  import { deriveIdentity, signChallenge, sealCell, openCell, shareCell, decodeShareEnvelope, unwrapSharedDek, verifyShareSig, openCellWithDek, verifyEnvelope, verifyIdentityRecord, encodeEnvelope, decodeEnvelope, encodeShareEnvelope, encodeIdentityRecord, decodeIdentityRecord, fromHex, toHex, utf8, fromUtf8, ctEqual, SeqHighWaterMark, } from '@saihm/client-pro';
5
5
  const REQUEST_TIMEOUT_MS = 30_000;
@@ -160,7 +160,9 @@ export class SaihmProClient {
160
160
  ? opts.requestTimeoutMs
161
161
  : REQUEST_TIMEOUT_MS;
162
162
  this.onboardBase = (opts.onboardBaseUrl ?? new URL(endpoint).origin).replace(/\/+$/, '');
163
- const trimmedAuth = typeof authHeader === 'string' && authHeader.trim() ? authHeader : undefined;
163
+ const trimmedAuth = typeof authHeader === 'string' && authHeader.trim()
164
+ ? authHeader
165
+ : undefined;
164
166
  if (trimmedAuth) {
165
167
  this.staticAuthHeader = trimmedAuth;
166
168
  }
@@ -189,7 +191,8 @@ export class SaihmProClient {
189
191
  throw new Error(`SAIHM_MASTER_SECRET_FILE could not be read: ${secretFile}`);
190
192
  }
191
193
  try {
192
- if (process.platform !== 'win32' && (statSync(secretFile).mode & 0o077) !== 0) {
194
+ if (process.platform !== 'win32' &&
195
+ (statSync(secretFile).mode & 0o077) !== 0) {
193
196
  process.stderr.write(`warning: SAIHM_MASTER_SECRET_FILE ${secretFile} is group/world-accessible; chmod 600 it.\n`);
194
197
  }
195
198
  }
@@ -288,12 +291,27 @@ export class SaihmProClient {
288
291
  if (this.tier === undefined) {
289
292
  throw new SaihmEndpointError(0, 'no_tier', 'join requires a tier (set SAIHM_TIER)');
290
293
  }
294
+ const ch = await this.onboardFetch(this.onboardBase + '/api/onboard/challenge', { method: 'GET' });
295
+ const nonce = ch.nonce;
296
+ if (typeof nonce !== 'string' || nonce.length === 0) {
297
+ throw new SaihmEndpointError(502, 'checkout_no_nonce', 'onboard challenge returned no nonce');
298
+ }
299
+ let nonceBytes;
300
+ try {
301
+ nonceBytes = fromHex(nonce);
302
+ }
303
+ catch {
304
+ throw new SaihmEndpointError(502, 'checkout_bad_nonce', 'onboard challenge nonce is not hex');
305
+ }
306
+ const signature = toHex(signChallenge(this.identity.mldsaSecretKey, nonceBytes));
291
307
  const out = await this.onboardFetch(this.onboardBase + '/api/stripe/checkout', {
292
308
  method: 'POST',
293
309
  headers: { 'content-type': 'application/json' },
294
310
  body: JSON.stringify({
295
311
  tier: this.tier,
296
312
  mldsaPubKey: toHex(this.identity.mldsaPubKey),
313
+ nonce,
314
+ signature,
297
315
  uiMode: 'hosted',
298
316
  }),
299
317
  });
@@ -317,7 +335,8 @@ export class SaihmProClient {
317
335
  }
318
336
  catch {
319
337
  }
320
- throw new SaihmEndpointError(res.status, code, `SAIHM onboard failed: ${res.status} ${res.statusText}` + (code ? ` (${code})` : ''));
338
+ throw new SaihmEndpointError(res.status, code, `SAIHM onboard failed: ${res.status} ${res.statusText}` +
339
+ (code ? ` (${code})` : ''));
321
340
  }
322
341
  try {
323
342
  return JSON.parse(text);
@@ -344,7 +363,9 @@ export class SaihmProClient {
344
363
  return await this.doCall(method, params, header);
345
364
  }
346
365
  catch (e) {
347
- if (!this.staticAuthHeader && e instanceof SaihmEndpointError && e.status === 401) {
366
+ if (!this.staticAuthHeader &&
367
+ e instanceof SaihmEndpointError &&
368
+ e.status === 401) {
348
369
  this.cachedJwt = undefined;
349
370
  const fresh = await this.currentAuthHeader();
350
371
  return await this.doCall(method, params, fresh);
package/dist/server.js CHANGED
@@ -82,13 +82,16 @@ server.tool('saihm_share', "Share a cell with another agent, end-to-end authenti
82
82
  recipientPinnedAgentIdHashHex: z
83
83
  .string()
84
84
  .describe("The grantee's agentIdHash (hex), pinned out-of-band"),
85
- scope: z.enum(['read', 'write', 'readwrite']).optional().describe('Access scope (default read)'),
85
+ scope: z
86
+ .enum(['read', 'write', 'readwrite'])
87
+ .optional()
88
+ .describe('Access scope (default read)'),
86
89
  expiryEpoch: z
87
90
  .string()
88
91
  .regex(/^[0-9]+$/, 'expiryEpoch must be a decimal UNIX-epoch count')
89
92
  .optional()
90
93
  .describe('Optional expiry as a UNIX-epoch count (decimal string)'),
91
- }, async ({ cellId, recipientRecord, recipientPinnedAgentIdHashHex, scope, expiryEpoch }) => {
94
+ }, async ({ cellId, recipientRecord, recipientPinnedAgentIdHashHex, scope, expiryEpoch, }) => {
92
95
  try {
93
96
  const r = await getClient().share({
94
97
  cellId,
@@ -105,7 +108,9 @@ server.tool('saihm_share', "Share a cell with another agent, end-to-end authenti
105
108
  });
106
109
  server.tool('saihm_revoke_share', 'Revoke a prior share grant to a recipient for a cell.', {
107
110
  cellId: z.string().describe('The shared cell id'),
108
- recipientHex: z.string().describe("The grantee's agentIdHash (hex) to revoke"),
111
+ recipientHex: z
112
+ .string()
113
+ .describe("The grantee's agentIdHash (hex) to revoke"),
109
114
  }, async ({ cellId, recipientHex }) => {
110
115
  try {
111
116
  const r = await getClient().revokeShare(cellId, recipientHex);
@@ -116,8 +121,13 @@ server.tool('saihm_revoke_share', 'Revoke a prior share grant to a recipient for
116
121
  }
117
122
  });
118
123
  server.tool('saihm_governance_propose', "Submit a gSAIHM governance proposal. Scope MUST be 'emission_param' or 'protocol_upgrade'.", {
119
- scope: z.enum(['emission_param', 'protocol_upgrade']).describe('Governable scope'),
120
- paramKey: z.string().optional().describe('Parameter key (when scope=emission_param)'),
124
+ scope: z
125
+ .enum(['emission_param', 'protocol_upgrade'])
126
+ .describe('Governable scope'),
127
+ paramKey: z
128
+ .string()
129
+ .optional()
130
+ .describe('Parameter key (when scope=emission_param)'),
121
131
  proposedValue: z.string().optional().describe('Proposed value as string'),
122
132
  }, async ({ scope, paramKey, proposedValue }) => {
123
133
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saihm/mcp-server-pro",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "SAIHM production thin-client. Seals client-side via @saihm/client-pro (ML-DSA-65 identity, per-cell AES-256-GCM DEK wrapped under a client KEK, ML-KEM-768 authenticated sharing) and POSTs opaque ciphertext to the blind, non-custodial SAIHM /mcp endpoint. The master secret, KEK, and plaintext never leave this process. Apache-2.0.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "clean": "rm -rf dist coverage",
19
19
  "build": "npm run clean && tsc -p . && chmod +x dist/server.js",
20
20
  "typecheck": "tsc -p tsconfig.test.json",
21
- "test": "tsx --test tests/client_pro.test.ts tests/server.test.ts",
21
+ "test": "tsx --test tests/smoke.test.ts tests/server.test.ts",
22
22
  "prepublishOnly": "npm run build && npm run typecheck && npm test"
23
23
  },
24
24
  "files": [