@mysten-incubation/memwal-mcp 0.0.13 → 0.0.14-dev.0

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.
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Recovery for a login that was interrupted after the browser registered our
3
+ * delegate key on-chain but before the callback could save it (WALM-332).
4
+ *
5
+ * `loginFlow` write-aheads the keypair to `login-pending.json` before the
6
+ * browser can act, so the key itself survives losing the process. What does
7
+ * not survive is the metadata the callback would have carried — `accountId`,
8
+ * `walletAddress`, `packageId` — and `credentials.json` is not loadable
9
+ * without them. `GET /api/whoami` closes that gap: the relayer resolves the
10
+ * account from the delegate key during authentication anyway, so it can hand
11
+ * back the identity the key already proves.
12
+ */
13
+ import { randomUUID, createHash } from "node:crypto";
14
+ import { loadCreds, saveCreds, loadPendingLogin, clearPendingLogin } from "./auth.js";
15
+ import { signMessage } from "./crypto.js";
16
+ import { log } from "./logger.js";
17
+ /** Same wall-clock budget as a normal cold-start probe: recovery must never
18
+ * be the reason a client hangs at startup. */
19
+ const WHOAMI_TIMEOUT_MS = 10_000;
20
+ /** `x-auth-error` value the relayer sets when it could not reach Sui to check
21
+ * the key at all. Named in `services/server/src/auth.rs`. */
22
+ const AUTH_UPSTREAM_UNAVAILABLE = "AUTH_UPSTREAM_UNAVAILABLE";
23
+ function isWhoami(o) {
24
+ if (!o || typeof o !== "object")
25
+ return false;
26
+ const w = o;
27
+ return (typeof w.account_id === "string" &&
28
+ /^0x[0-9a-fA-F]{64}$/.test(w.account_id) &&
29
+ typeof w.owner === "string" &&
30
+ typeof w.package_id === "string");
31
+ }
32
+ /**
33
+ * Build the exact string the relayer will rebuild and verify against.
34
+ *
35
+ * `services/server/src/auth.rs` calls itself the single source of truth for
36
+ * this format, and it is reproduced here rather than imported because the two
37
+ * live in different languages. That duplication is the risk: get it subtly
38
+ * wrong — a trimmed trailing separator, a missing empty field — and every
39
+ * recovery attempt fails with an opaque 401 that no type checker would have
40
+ * caught. Exported so a test can pin it against the identical literal asserted
41
+ * in `routes::accounts::tests::whoami_recovery_request_canonical_message_is_stable`.
42
+ *
43
+ * `accountId` is empty for recovery: not knowing it is the reason we are here,
44
+ * and the server defaults its hint to `""` when the header is absent.
45
+ */
46
+ export function canonicalRequestMessage(parts) {
47
+ const { timestamp, method, path, bodyHash, nonce, accountId = "" } = parts;
48
+ return `${timestamp}.${method}.${path}.${bodyHash}.${nonce}.${accountId}`;
49
+ }
50
+ /** sha256 of an empty body. A GET sends none; the server hashes it anyway. */
51
+ export const EMPTY_BODY_SHA256 = createHash("sha256").update("").digest("hex");
52
+ /**
53
+ * Ask the relayer who this delegate key belongs to.
54
+ *
55
+ * The account id is signed as an empty string and its header omitted, because
56
+ * not knowing it is the entire reason we are here. The server defaults the
57
+ * hint to `""` when the header is absent, so both sides build the same
58
+ * canonical message.
59
+ *
60
+ * Returns null only when the request never produced a response at all.
61
+ */
62
+ async function whoami(relayerUrl, privateKeyHex, publicKeyHex) {
63
+ const path = "/api/whoami";
64
+ // SECONDS. `services/server/src/auth.rs` freshness-checks `x-timestamp`
65
+ // against `chrono::Utc::now().timestamp()` within a drift window of a few
66
+ // minutes, so a millisecond value (~10^12) is always outside it and every
67
+ // request 401s with ERR_TIMESTAMP_OUT_OF_BOUNDS.
68
+ const timestamp = Math.floor(Date.now() / 1000).toString();
69
+ const nonce = randomUUID();
70
+ const message = canonicalRequestMessage({
71
+ timestamp,
72
+ method: "GET",
73
+ path,
74
+ bodyHash: EMPTY_BODY_SHA256,
75
+ nonce,
76
+ });
77
+ const signature = await signMessage(privateKeyHex, message);
78
+ const controller = new AbortController();
79
+ const timer = setTimeout(() => controller.abort(), WHOAMI_TIMEOUT_MS);
80
+ timer.unref?.();
81
+ try {
82
+ const resp = await fetch(`${relayerUrl.replace(/\/+$/, "")}${path}`, {
83
+ method: "GET",
84
+ headers: {
85
+ "x-public-key": publicKeyHex,
86
+ "x-signature": signature,
87
+ "x-timestamp": timestamp,
88
+ "x-nonce": nonce,
89
+ },
90
+ signal: controller.signal,
91
+ });
92
+ const text = await resp.text();
93
+ let body = null;
94
+ try {
95
+ body = JSON.parse(text);
96
+ }
97
+ catch {
98
+ /* non-JSON error page — status is what matters */
99
+ }
100
+ return {
101
+ status: resp.status,
102
+ body,
103
+ authError: resp.headers.get("x-auth-error"),
104
+ };
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ finally {
110
+ clearTimeout(timer);
111
+ }
112
+ }
113
+ /**
114
+ * Attempt to turn a stranded pending login into usable credentials.
115
+ *
116
+ * Never throws, and never deletes a record that might still be recoverable —
117
+ * a stranded key is the user's paid-for property, and the cost of keeping it
118
+ * around until its TTL is a file that grants nothing.
119
+ */
120
+ export async function recoverPendingLogin() {
121
+ const pending = loadPendingLogin();
122
+ if (!pending)
123
+ return { outcome: "no-pending" };
124
+ // Ordering guard. If a later sign-in already succeeded, its credentials
125
+ // are the user's current intent and must not be rolled back to an older
126
+ // stranded key. Note this compares against the *pending* record's start
127
+ // time, so a login begun after the last successful one still wins.
128
+ const existing = loadCreds();
129
+ if (existing && Date.parse(existing.createdAt) >= Date.parse(pending.createdAt)) {
130
+ log.warn("login.pending.superseded", {
131
+ publicKey: pending.delegatePublicKeyHex,
132
+ });
133
+ clearPendingLogin();
134
+ return { outcome: "superseded", strandedPublicKey: pending.delegatePublicKeyHex };
135
+ }
136
+ const res = await whoami(pending.relayerUrl, pending.delegatePrivateKey, pending.delegatePublicKeyHex);
137
+ if (res === null) {
138
+ log.warn("login.pending.relayer_unreachable", {
139
+ publicKey: pending.delegatePublicKeyHex,
140
+ });
141
+ return { outcome: "unavailable", strandedPublicKey: pending.delegatePublicKeyHex };
142
+ }
143
+ if (res.status !== 200 || !isWhoami(res.body)) {
144
+ // `rejected` is reserved for the relayer actually denying this
145
+ // identity, because that is the only outcome whose advice — sign in
146
+ // again, after removing the key from the dashboard if it was already
147
+ // registered — is worth giving. During a transient upstream
148
+ // failure that advice is worse than silence: the key is still good, and
149
+ // `unavailable` correctly says the next start retries it with no action
150
+ // from the user.
151
+ //
152
+ // So only 401/403 is a denial. A 503 carrying
153
+ // `x-auth-error: AUTH_UPSTREAM_UNAVAILABLE` is Sui RPC being down, and
154
+ // 429 / 5xx / a 404 from a relayer too old to serve this route are all
155
+ // "ask again later" — as is a 200 whose body is not a whoami, which
156
+ // means we are not talking to the endpoint we think we are.
157
+ const denied = (res.status === 401 || res.status === 403) &&
158
+ res.authError !== AUTH_UPSTREAM_UNAVAILABLE;
159
+ const outcome = denied ? "rejected" : "unavailable";
160
+ // Non-destructive either way. A 401 is ambiguous even when it IS a
161
+ // denial: on testnet the registry scan is disabled outright and a
162
+ // genuinely registered key is refused for want of an x-account-id hint
163
+ // (services/server/src/auth.rs — "x-account-id is required for
164
+ // delegate-key authentication on testnet"). Clearing here would destroy
165
+ // a recoverable key in exactly that environment, so the record is
166
+ // always left for its TTL to retire.
167
+ log.warn(`login.pending.${outcome}`, {
168
+ publicKey: pending.delegatePublicKeyHex,
169
+ status: res.status,
170
+ authError: res.authError,
171
+ });
172
+ return { outcome, strandedPublicKey: pending.delegatePublicKeyHex };
173
+ }
174
+ const creds = {
175
+ delegatePrivateKey: pending.delegatePrivateKey,
176
+ delegatePublicKeyHex: pending.delegatePublicKeyHex,
177
+ delegateAddress: pending.delegateAddress,
178
+ walletAddress: res.body.owner,
179
+ accountId: res.body.account_id,
180
+ packageId: res.body.package_id,
181
+ relayerUrl: pending.relayerUrl,
182
+ label: pending.label,
183
+ createdAt: new Date().toISOString(),
184
+ version: 1,
185
+ };
186
+ saveCreds(creds);
187
+ clearPendingLogin();
188
+ log.info("login.pending.recovered", {
189
+ accountId: creds.accountId,
190
+ delegateAddress: creds.delegateAddress,
191
+ });
192
+ return { outcome: "recovered", credentials: creds };
193
+ }
194
+ /**
195
+ * The line to show the user when a stranded key could not be reclaimed.
196
+ *
197
+ * Names the key so the user can find the registration they paid for in the
198
+ * dashboard. Signing in again reuses this key, and the dashboard's
199
+ * `add_delegate_key` aborts on one that is already registered, so a key the
200
+ * user approved has to be removed there before a new sign-in can finish. A
201
+ * user told only "login failed" can do neither.
202
+ */
203
+ export function formatStrandedLoginNotice(result) {
204
+ if (!result.strandedPublicKey)
205
+ return null;
206
+ if (result.outcome === "recovered" || result.outcome === "no-pending")
207
+ return null;
208
+ const key = result.strandedPublicKey;
209
+ const lines = [
210
+ `⚠️ Your last Walrus Memory sign-in did not finish.`,
211
+ ``,
212
+ `A delegate key may have been registered on-chain without being saved locally:`,
213
+ ` ${key}`,
214
+ ``,
215
+ ];
216
+ if (result.outcome === "superseded") {
217
+ lines.push(`You have since signed in again, so your current credentials are fine.`, `Revoke the key above from the dashboard if you don't recognise it.`);
218
+ }
219
+ else if (result.outcome === "unavailable") {
220
+ lines.push(`The relayer could not be reached to check. This will be retried on the`, `next start — no action needed yet.`);
221
+ }
222
+ else {
223
+ lines.push(`The relayer did not accept it. If you never approved the wallet step, run`, `\`memwal_login\`: it reuses this key. If you did, remove the key above from`, `the dashboard first and then run \`memwal_login\`, because the wallet step`, `cannot register a key that is already there. This is expected on Testnet,`, `where the relayer cannot confirm a registered key at start.`);
224
+ }
225
+ return lines.join("\n");
226
+ }
227
+ //# sourceMappingURL=recovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recovery.js","sourceRoot":"","sources":["../src/recovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,48 @@
1
+ import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
2
+ import type { MemWalCredentials } from "./auth.js";
3
+ /** Which relayer transport the bridge dials. */
4
+ export type TransportKind = "sse" | "http";
5
+ /**
6
+ * Resolve the transport from `MEMWAL_MCP_TRANSPORT`.
7
+ *
8
+ * Defaults to `sse`, the transport every released bridge has used. Streamable
9
+ * HTTP is opt-in until it has production mileage: this runs on users' machines
10
+ * against their real memories, so the new path proves itself before it becomes
11
+ * the one that runs by default.
12
+ *
13
+ * An unrecognised value falls back rather than throwing — a typo in a user's
14
+ * MCP config must not stop their memory from working.
15
+ */
16
+ export declare function resolveTransport(raw: string | undefined): TransportKind;
17
+ /**
18
+ * The Streamable HTTP endpoint for a relayer base URL.
19
+ *
20
+ * `/api/mcp` — the same base the SSE transport hangs `/api/mcp/sse` and
21
+ * `/api/mcp/messages` off, minus the split.
22
+ */
23
+ export declare function streamableUrl(relayerUrl: string): string;
24
+ /**
25
+ * A live relayer session. Deliberately the same shape the SSE handshake
26
+ * returns, so `runBridge` can hold either without branching on transport
27
+ * everywhere it forwards a message.
28
+ */
29
+ export interface RelaySession {
30
+ /** Endpoint this session talks to. Logging only. */
31
+ postUrl: string;
32
+ /**
33
+ * Forward one JSON-RPC message. Resolves with an HTTP-ish status the
34
+ * caller can act on: 200 for accepted, 404 when the relayer says the
35
+ * session does not exist (the message provably did not run, so the
36
+ * caller may retry it without risking a duplicate write).
37
+ */
38
+ send(msg: JSONRPCMessage,
39
+ /** Unused here — headers are bound when the session opens. Present so
40
+ * this matches the SSE handshake's `send`, which signs per POST. */
41
+ creds?: MemWalCredentials, extra?: Record<string, string>): Promise<number>;
42
+ /** Incoming messages from the relayer. */
43
+ iter: AsyncIterator<JSONRPCMessage>;
44
+ /** Tear the session down. */
45
+ abort: () => void;
46
+ }
47
+ export declare function openStreamableSession(relayerUrl: string, creds: MemWalCredentials, extraHeaders?: Record<string, string>): Promise<RelaySession>;
48
+ //# sourceMappingURL=streamable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamable.d.ts","sourceRoot":"","sources":["../src/streamable.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGnD,gDAAgD;AAChD,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CASvE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IACzB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;OAKG;IACH,IAAI,CACA,GAAG,EAAE,cAAc;IACnB;wEACoE;IACpE,KAAK,CAAC,EAAE,iBAAiB,EACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAAC,MAAM,CAAC,CAAC;IACnB,0CAA0C;IAC1C,IAAI,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACpC,6BAA6B;IAC7B,KAAK,EAAE,MAAM,IAAI,CAAC;CACrB;AAQD,wBAAsB,qBAAqB,CACvC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,iBAAiB,EACxB,YAAY,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAC1C,OAAO,CAAC,YAAY,CAAC,CA+EvB"}
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Streamable HTTP transport for the stdio bridge.
3
+ *
4
+ * The legacy transport splits a call in two: POST to `/api/mcp/messages`, then
5
+ * wait for the reply to arrive on a separate `/api/mcp/sse` stream. Everything
6
+ * expensive in `bridge.ts` follows from that split — the `inFlight` map, the
7
+ * `sent` flag, the 404-means-never-ran reset, the idle watchdog, and the
8
+ * replay-on-reconnect path — because a POST can succeed while its reply is
9
+ * lost, and the bridge cannot tell that from a call still running.
10
+ *
11
+ * Streamable HTTP (MCP 2025-06) collapses that: one endpoint, and the reply
12
+ * comes back on the same request. The relayer has served it since
13
+ * `mcp_proxy.rs:751` ("Single endpoint that supersedes the SSE+POST split");
14
+ * only the bridge was still on the old transport.
15
+ *
16
+ * This module wraps the MCP SDK's own client transport rather than hand-rolling
17
+ * the protocol: session-id round-tripping, the optional SSE upgrade on a POST
18
+ * response, and resumption tokens are all spec details that are easy to get
19
+ * subtly wrong and that the SDK already implements.
20
+ */
21
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
22
+ import { log } from "./logger.js";
23
+ /**
24
+ * Resolve the transport from `MEMWAL_MCP_TRANSPORT`.
25
+ *
26
+ * Defaults to `sse`, the transport every released bridge has used. Streamable
27
+ * HTTP is opt-in until it has production mileage: this runs on users' machines
28
+ * against their real memories, so the new path proves itself before it becomes
29
+ * the one that runs by default.
30
+ *
31
+ * An unrecognised value falls back rather than throwing — a typo in a user's
32
+ * MCP config must not stop their memory from working.
33
+ */
34
+ export function resolveTransport(raw) {
35
+ const value = raw?.trim().toLowerCase();
36
+ if (!value)
37
+ return "sse";
38
+ if (value === "http" || value === "streamable" || value === "streamable-http") {
39
+ return "http";
40
+ }
41
+ if (value === "sse")
42
+ return "sse";
43
+ log.warn("bridge.transport_unrecognized", { value, using: "sse" });
44
+ return "sse";
45
+ }
46
+ /**
47
+ * The Streamable HTTP endpoint for a relayer base URL.
48
+ *
49
+ * `/api/mcp` — the same base the SSE transport hangs `/api/mcp/sse` and
50
+ * `/api/mcp/messages` off, minus the split.
51
+ */
52
+ export function streamableUrl(relayerUrl) {
53
+ return `${relayerUrl.replace(/\/+$/, "")}/api/mcp`;
54
+ }
55
+ /** HTTP status carried on the SDK's transport error, when it has one. */
56
+ function statusOf(err) {
57
+ const code = err?.code;
58
+ return typeof code === "number" ? code : 0;
59
+ }
60
+ export async function openStreamableSession(relayerUrl, creds, extraHeaders = {}) {
61
+ const url = streamableUrl(relayerUrl);
62
+ // Queue + waiter rather than an event emitter, so a message that arrives
63
+ // before `runBridge` pulls from the iterator is buffered instead of
64
+ // dropped. The SSE path does the same thing for the same reason.
65
+ const queue = [];
66
+ let wake = null;
67
+ let closed = false;
68
+ const push = (msg) => {
69
+ queue.push(msg);
70
+ const resume = wake;
71
+ wake = null;
72
+ resume?.();
73
+ };
74
+ const finish = () => {
75
+ closed = true;
76
+ const resume = wake;
77
+ wake = null;
78
+ resume?.();
79
+ };
80
+ const transport = new StreamableHTTPClientTransport(new URL(url), {
81
+ requestInit: {
82
+ headers: {
83
+ authorization: `Bearer ${creds.delegatePrivateKey}`,
84
+ "x-memwal-account-id": creds.accountId,
85
+ ...extraHeaders,
86
+ },
87
+ },
88
+ });
89
+ transport.onmessage = push;
90
+ transport.onclose = finish;
91
+ transport.onerror = (err) => {
92
+ log.warn("bridge.streamable_error", { err: String(err) });
93
+ // Not `finish()` — the SDK transport reconnects its own stream, and
94
+ // tearing the session down on a transient read error is what the SSE
95
+ // watchdog did wrong.
96
+ };
97
+ await transport.start();
98
+ const iter = {
99
+ async next() {
100
+ while (queue.length === 0) {
101
+ if (closed)
102
+ return { value: undefined, done: true };
103
+ await new Promise((resolve) => (wake = resolve));
104
+ }
105
+ return { value: queue.shift(), done: false };
106
+ },
107
+ };
108
+ return {
109
+ postUrl: url,
110
+ async send(msg) {
111
+ try {
112
+ await transport.send(msg);
113
+ return 200;
114
+ }
115
+ catch (err) {
116
+ const status = statusOf(err);
117
+ log.warn("bridge.streamable_send_failed", {
118
+ status,
119
+ err: String(err),
120
+ });
121
+ // Surface the status rather than throwing: `postIfCurrent`
122
+ // routes on it, and a 404 specifically means the message was
123
+ // discarded rather than run.
124
+ return status;
125
+ }
126
+ },
127
+ iter,
128
+ abort: () => {
129
+ void transport.close().catch(() => {
130
+ /* already gone */
131
+ });
132
+ finish();
133
+ },
134
+ };
135
+ }
136
+ //# sourceMappingURL=streamable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"streamable.js","sourceRoot":"","sources":["../src/streamable.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mysten-incubation/memwal-mcp",
3
- "version": "0.0.13",
3
+ "version": "0.0.14-dev.0",
4
4
  "description": "Walrus Memory MCP client — single-binary stdio MCP server that bridges Cursor / Claude Desktop / Antigravity / Claude Code to the Walrus Memory relayer. Handles browser-based wallet login on first run.",
5
5
  "type": "module",
6
6
  "engines": {