@saihm/mcp-server-pro 0.1.5 → 0.1.6
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 +11 -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
|
}
|
|
@@ -317,7 +320,8 @@ export class SaihmProClient {
|
|
|
317
320
|
}
|
|
318
321
|
catch {
|
|
319
322
|
}
|
|
320
|
-
throw new SaihmEndpointError(res.status, code, `SAIHM onboard failed: ${res.status} ${res.statusText}` +
|
|
323
|
+
throw new SaihmEndpointError(res.status, code, `SAIHM onboard failed: ${res.status} ${res.statusText}` +
|
|
324
|
+
(code ? ` (${code})` : ''));
|
|
321
325
|
}
|
|
322
326
|
try {
|
|
323
327
|
return JSON.parse(text);
|
|
@@ -344,7 +348,9 @@ export class SaihmProClient {
|
|
|
344
348
|
return await this.doCall(method, params, header);
|
|
345
349
|
}
|
|
346
350
|
catch (e) {
|
|
347
|
-
if (!this.staticAuthHeader &&
|
|
351
|
+
if (!this.staticAuthHeader &&
|
|
352
|
+
e instanceof SaihmEndpointError &&
|
|
353
|
+
e.status === 401) {
|
|
348
354
|
this.cachedJwt = undefined;
|
|
349
355
|
const fresh = await this.currentAuthHeader();
|
|
350
356
|
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.6",
|
|
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": [
|