@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.
- package/dist/auth-required.d.ts +110 -0
- package/dist/auth-required.d.ts.map +1 -1
- package/dist/auth-required.js +47 -3
- package/dist/auth-required.js.map +1 -1
- package/dist/auth.d.ts +60 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +135 -0
- package/dist/auth.js.map +1 -1
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +117 -22
- package/dist/bridge.js.map +1 -1
- package/dist/crypto.d.ts +9 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +13 -1
- package/dist/crypto.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -1
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +9 -0
- package/dist/instructions.js.map +1 -1
- package/dist/login.d.ts +5 -1
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +78 -11
- package/dist/login.js.map +1 -1
- package/dist/messages.d.ts +5 -0
- package/dist/messages.d.ts.map +1 -1
- package/dist/messages.js +11 -4
- package/dist/messages.js.map +1 -1
- package/dist/recovery.d.ts +61 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +227 -0
- package/dist/recovery.js.map +1 -0
- package/dist/streamable.d.ts +48 -0
- package/dist/streamable.d.ts.map +1 -0
- package/dist/streamable.js +136 -0
- package/dist/streamable.js.map +1 -0
- package/package.json +1 -1
package/dist/recovery.js
ADDED
|
@@ -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":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAInG,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAKlC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAuB;IACpD,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,iBAAiB,EAAE,CAAC;QAC5E,OAAO,MAAM,CAAC;IAClB,CAAC;IACD,IAAI,KAAK,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAClC,GAAG,CAAC,IAAI,CAAC,+BAA+B,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IACnE,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,UAAkB;IAC5C,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,CAAC;AACvD,CAAC;AA6BD,yEAAyE;AACzE,SAAS,QAAQ,CAAC,GAAY;IAC1B,MAAM,IAAI,GAAI,GAAiC,EAAE,IAAI,CAAC;IACtD,OAAO,OAAO,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,qBAAqB,CACvC,UAAkB,EAClB,KAAwB,EACxB,eAAuC,EAAE;IAEzC,MAAM,GAAG,GAAG,aAAa,CAAC,UAAU,CAAC,CAAC;IAEtC,yEAAyE;IACzE,oEAAoE;IACpE,iEAAiE;IACjE,MAAM,KAAK,GAAqB,EAAE,CAAC;IACnC,IAAI,IAAI,GAAwB,IAAI,CAAC;IACrC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,MAAM,IAAI,GAAG,CAAC,GAAmB,EAAE,EAAE;QACjC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChB,MAAM,MAAM,GAAG,IAAI,CAAC;QACpB,IAAI,GAAG,IAAI,CAAC;QACZ,MAAM,EAAE,EAAE,CAAC;IACf,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,GAAG,EAAE;QAChB,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,MAAM,GAAG,IAAI,CAAC;QACpB,IAAI,GAAG,IAAI,CAAC;QACZ,MAAM,EAAE,EAAE,CAAC;IACf,CAAC,CAAC;IAEF,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,EAAE;QAC9D,WAAW,EAAE;YACT,OAAO,EAAE;gBACL,aAAa,EAAE,UAAU,KAAK,CAAC,kBAAkB,EAAE;gBACnD,qBAAqB,EAAE,KAAK,CAAC,SAAS;gBACtC,GAAG,YAAY;aAClB;SACJ;KACJ,CAAC,CAAC;IAEH,SAAS,CAAC,SAAS,GAAG,IAAI,CAAC;IAC3B,SAAS,CAAC,OAAO,GAAG,MAAM,CAAC;IAC3B,SAAS,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE;QACxB,GAAG,CAAC,IAAI,CAAC,yBAAyB,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC1D,oEAAoE;QACpE,qEAAqE;QACrE,sBAAsB;IAC1B,CAAC,CAAC;IAEF,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;IAExB,MAAM,IAAI,GAAkC;QACxC,KAAK,CAAC,IAAI;YACN,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxB,IAAI,MAAM;oBAAE,OAAO,EAAE,KAAK,EAAE,SAAkB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;gBAC7D,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC;YAC3D,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAG,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QAClD,CAAC;KACJ,CAAC;IAEF,OAAO;QACH,OAAO,EAAE,GAAG;QACZ,KAAK,CAAC,IAAI,CAAC,GAAG;YACV,IAAI,CAAC;gBACD,MAAM,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC1B,OAAO,GAAG,CAAC;YACf,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACX,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC7B,GAAG,CAAC,IAAI,CAAC,+BAA+B,EAAE;oBACtC,MAAM;oBACN,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC;iBACnB,CAAC,CAAC;gBACH,2DAA2D;gBAC3D,6DAA6D;gBAC7D,6BAA6B;gBAC7B,OAAO,MAAM,CAAC;YAClB,CAAC;QACL,CAAC;QACD,IAAI;QACJ,KAAK,EAAE,GAAG,EAAE;YACR,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE;gBAC9B,kBAAkB;YACtB,CAAC,CAAC,CAAC;YACH,MAAM,EAAE,CAAC;QACb,CAAC;KACJ,CAAC;AACN,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mysten-incubation/memwal-mcp",
|
|
3
|
-
"version": "0.0.
|
|
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": {
|