@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 +18 -13
- package/dist/client.js +26 -5
- package/dist/server.js +15 -5
- package/package.json +2 -2
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
|
|
121
|
-
|
|
|
122
|
-
| `SAIHM_ENDPOINT_URL`
|
|
123
|
-
| `SAIHM_AUTH_HEADER`
|
|
124
|
-
| `SAIHM_PAYMENT_METHOD`
|
|
125
|
-
| `SAIHM_MASTER_SECRET_HEX`
|
|
126
|
-
| `SAIHM_MASTER_SECRET_FILE
|
|
127
|
-
| `SAIHM_TIER`
|
|
128
|
-
| `SAIHM_SEQ_STATE_PATH`
|
|
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()
|
|
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' &&
|
|
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}` +
|
|
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 &&
|
|
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
|
|
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
|
|
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
|
|
120
|
-
|
|
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.
|
|
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/
|
|
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": [
|