signupgenius-mcp 1.0.3 → 1.0.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/.claude-plugin/marketplace.json +34 -0
- package/.claude-plugin/plugin.json +19 -0
- package/README.md +22 -0
- package/dist/bundle.js +404 -92
- package/dist/index.js +1 -1
- package/dist/tools/rsvp.js +41 -9
- package/package.json +8 -8
- package/server.json +2 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
3
|
+
"name": "signupgenius-mcp",
|
|
4
|
+
"owner": {
|
|
5
|
+
"name": "Chris Hall",
|
|
6
|
+
"email": "chris.c.hall@gmail.com"
|
|
7
|
+
},
|
|
8
|
+
"metadata": {
|
|
9
|
+
"description": "MCP server for SignUpGenius — read sign-ups, slot reports, and groups; add group members.",
|
|
10
|
+
"version": "1.0.7"
|
|
11
|
+
},
|
|
12
|
+
"plugins": [
|
|
13
|
+
{
|
|
14
|
+
"name": "signupgenius-mcp",
|
|
15
|
+
"displayName": "SignUpGenius",
|
|
16
|
+
"source": "./",
|
|
17
|
+
"description": "SignUpGenius MCP server for Claude — sign-ups, slot reports, and groups via natural language. Free or Pro accounts.",
|
|
18
|
+
"version": "1.0.7",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Chris Hall"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/chrischall/signupgenius-mcp",
|
|
23
|
+
"repository": "https://github.com/chrischall/signupgenius-mcp",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"keywords": [
|
|
26
|
+
"signupgenius",
|
|
27
|
+
"signups",
|
|
28
|
+
"volunteers",
|
|
29
|
+
"mcp"
|
|
30
|
+
],
|
|
31
|
+
"category": "productivity"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signupgenius-mcp",
|
|
3
|
+
"displayName": "SignUpGenius",
|
|
4
|
+
"version": "1.0.7",
|
|
5
|
+
"description": "SignUpGenius MCP server for Claude — read sign-ups, slot reports, and groups; add group members. Three auth paths: Pro API key, email/password, or sign in via the fetchproxy browser extension.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Chris Hall",
|
|
8
|
+
"email": "chris.c.hall@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/chrischall/signupgenius-mcp",
|
|
11
|
+
"repository": "https://github.com/chrischall/signupgenius-mcp",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"signupgenius",
|
|
15
|
+
"signups",
|
|
16
|
+
"volunteers",
|
|
17
|
+
"mcp"
|
|
18
|
+
]
|
|
19
|
+
}
|
package/README.md
CHANGED
|
@@ -88,3 +88,25 @@ Tests: vitest, 100% line/branch/function coverage. End-to-end tests against the
|
|
|
88
88
|
- For testing the Pro v2/k surface without an account, SignUpGenius publishes a frozen demo key: `V0FzMkxZcmVOZlVnclZMVEl6dGhWQT09`.
|
|
89
89
|
|
|
90
90
|
Developed and maintained by AI (Claude). Use at your own discretion.
|
|
91
|
+
|
|
92
|
+
## Acknowledgement of Terms
|
|
93
|
+
|
|
94
|
+
By using this MCP server, you acknowledge and agree to the following:
|
|
95
|
+
|
|
96
|
+
**1. This server accesses your own SignUpGenius account.** Auth happens via your own credentials. It does not — and cannot — access anyone else's account or signups.
|
|
97
|
+
|
|
98
|
+
**2. [SignUpGenius's Terms of Service](https://www.signupgenius.com/terms-of-service) govern your use of this server**, just as they govern your direct use of signupgenius.com. The clauses most relevant here:
|
|
99
|
+
|
|
100
|
+
> Users may not bypass any robot exclusion headers or other measures we take to restrict access to the Services or use any software, technology, or device to scrape, spider, or crawl the Services.
|
|
101
|
+
|
|
102
|
+
And: *"You are responsible for maintaining the confidentiality of your account user name and password… You agree to accept responsibility for any and all activities or actions that occur in connection with your User Credentials."*
|
|
103
|
+
|
|
104
|
+
You are agreeing to those terms — read by the maintainer 2026-05-23 — every time you invoke a tool in this server. Notably, **SignUpGenius does offer an official API** for paid plans; where possible, prefer the official API over the endpoints this MCP exercises.
|
|
105
|
+
|
|
106
|
+
**3. Personal, organizer/participant use only.** This project is not affiliated with, endorsed by, sponsored by, or in partnership with SignUpGenius, Inc. It is a personal automation tool for an authenticated user to manage their own signups and groups. Do not use it to scrape other organizers' signups, spam participants, or bulk-add fake group members.
|
|
107
|
+
|
|
108
|
+
**4. Stability is not guaranteed.** This server may call internal endpoints that SignUpGenius can change without notice. If a tool here breaks, the canonical fix is to use the official API where available.
|
|
109
|
+
|
|
110
|
+
**5. You accept full responsibility** for any consequences of using this server in connection with your SignUpGenius account — rate limiting, account warnings, suspension, or any enforcement action. Per the ToS, everything done under your credentials is attributed to you. If SignUpGenius objects to your use, stop using this server.
|
|
111
|
+
|
|
112
|
+
This section is the maintainer's good-faith summary of the terms — it is not legal advice and does not modify or supersede SignUpGenius's actual ToS.
|
package/dist/bundle.js
CHANGED
|
@@ -7659,6 +7659,10 @@ var require_receiver = __commonJS({
|
|
|
7659
7659
|
* extensions
|
|
7660
7660
|
* @param {Boolean} [options.isServer=false] Specifies whether to operate in
|
|
7661
7661
|
* client or server mode
|
|
7662
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
7663
|
+
* buffered data chunks
|
|
7664
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
7665
|
+
* fragments
|
|
7662
7666
|
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
7663
7667
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
7664
7668
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -7669,6 +7673,8 @@ var require_receiver = __commonJS({
|
|
|
7669
7673
|
this._binaryType = options.binaryType || BINARY_TYPES[0];
|
|
7670
7674
|
this._extensions = options.extensions || {};
|
|
7671
7675
|
this._isServer = !!options.isServer;
|
|
7676
|
+
this._maxBufferedChunks = options.maxBufferedChunks | 0;
|
|
7677
|
+
this._maxFragments = options.maxFragments | 0;
|
|
7672
7678
|
this._maxPayload = options.maxPayload | 0;
|
|
7673
7679
|
this._skipUTF8Validation = !!options.skipUTF8Validation;
|
|
7674
7680
|
this[kWebSocket] = void 0;
|
|
@@ -7698,6 +7704,18 @@ var require_receiver = __commonJS({
|
|
|
7698
7704
|
*/
|
|
7699
7705
|
_write(chunk, encoding, cb) {
|
|
7700
7706
|
if (this._opcode === 8 && this._state == GET_INFO) return cb();
|
|
7707
|
+
if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
|
|
7708
|
+
cb(
|
|
7709
|
+
this.createError(
|
|
7710
|
+
RangeError,
|
|
7711
|
+
"Too many buffered chunks",
|
|
7712
|
+
false,
|
|
7713
|
+
1008,
|
|
7714
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
7715
|
+
)
|
|
7716
|
+
);
|
|
7717
|
+
return;
|
|
7718
|
+
}
|
|
7701
7719
|
this._bufferedBytes += chunk.length;
|
|
7702
7720
|
this._buffers.push(chunk);
|
|
7703
7721
|
this.startLoop(cb);
|
|
@@ -8027,6 +8045,17 @@ var require_receiver = __commonJS({
|
|
|
8027
8045
|
return;
|
|
8028
8046
|
}
|
|
8029
8047
|
if (data.length) {
|
|
8048
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8049
|
+
const error51 = this.createError(
|
|
8050
|
+
RangeError,
|
|
8051
|
+
"Too many message fragments",
|
|
8052
|
+
false,
|
|
8053
|
+
1008,
|
|
8054
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8055
|
+
);
|
|
8056
|
+
cb(error51);
|
|
8057
|
+
return;
|
|
8058
|
+
}
|
|
8030
8059
|
this._messageLength = this._totalPayloadLength;
|
|
8031
8060
|
this._fragments.push(data);
|
|
8032
8061
|
}
|
|
@@ -8056,6 +8085,17 @@ var require_receiver = __commonJS({
|
|
|
8056
8085
|
cb(error51);
|
|
8057
8086
|
return;
|
|
8058
8087
|
}
|
|
8088
|
+
if (this._maxFragments > 0 && this._fragments.length >= this._maxFragments) {
|
|
8089
|
+
const error51 = this.createError(
|
|
8090
|
+
RangeError,
|
|
8091
|
+
"Too many message fragments",
|
|
8092
|
+
false,
|
|
8093
|
+
1008,
|
|
8094
|
+
"WS_ERR_TOO_MANY_BUFFERED_PARTS"
|
|
8095
|
+
);
|
|
8096
|
+
cb(error51);
|
|
8097
|
+
return;
|
|
8098
|
+
}
|
|
8059
8099
|
this._fragments.push(buf);
|
|
8060
8100
|
}
|
|
8061
8101
|
this.dataMessage(cb);
|
|
@@ -9262,6 +9302,10 @@ var require_websocket = __commonJS({
|
|
|
9262
9302
|
* multiple times in the same tick
|
|
9263
9303
|
* @param {Function} [options.generateMask] The function used to generate the
|
|
9264
9304
|
* masking key
|
|
9305
|
+
* @param {Number} [options.maxBufferedChunks=0] The maximum number of
|
|
9306
|
+
* buffered data chunks
|
|
9307
|
+
* @param {Number} [options.maxFragments=0] The maximum number of message
|
|
9308
|
+
* fragments
|
|
9265
9309
|
* @param {Number} [options.maxPayload=0] The maximum allowed message size
|
|
9266
9310
|
* @param {Boolean} [options.skipUTF8Validation=false] Specifies whether or
|
|
9267
9311
|
* not to skip UTF-8 validation for text and close messages
|
|
@@ -9273,6 +9317,8 @@ var require_websocket = __commonJS({
|
|
|
9273
9317
|
binaryType: this.binaryType,
|
|
9274
9318
|
extensions: this._extensions,
|
|
9275
9319
|
isServer: this._isServer,
|
|
9320
|
+
maxBufferedChunks: options.maxBufferedChunks,
|
|
9321
|
+
maxFragments: options.maxFragments,
|
|
9276
9322
|
maxPayload: options.maxPayload,
|
|
9277
9323
|
skipUTF8Validation: options.skipUTF8Validation
|
|
9278
9324
|
});
|
|
@@ -9572,6 +9618,8 @@ var require_websocket = __commonJS({
|
|
|
9572
9618
|
autoPong: true,
|
|
9573
9619
|
closeTimeout: CLOSE_TIMEOUT,
|
|
9574
9620
|
protocolVersion: protocolVersions[1],
|
|
9621
|
+
maxBufferedChunks: 1024 * 1024,
|
|
9622
|
+
maxFragments: 128 * 1024,
|
|
9575
9623
|
maxPayload: 100 * 1024 * 1024,
|
|
9576
9624
|
skipUTF8Validation: false,
|
|
9577
9625
|
perMessageDeflate: true,
|
|
@@ -9814,6 +9862,8 @@ var require_websocket = __commonJS({
|
|
|
9814
9862
|
websocket.setSocket(socket, head, {
|
|
9815
9863
|
allowSynchronousEvents: opts.allowSynchronousEvents,
|
|
9816
9864
|
generateMask: opts.generateMask,
|
|
9865
|
+
maxBufferedChunks: opts.maxBufferedChunks,
|
|
9866
|
+
maxFragments: opts.maxFragments,
|
|
9817
9867
|
maxPayload: opts.maxPayload,
|
|
9818
9868
|
skipUTF8Validation: opts.skipUTF8Validation
|
|
9819
9869
|
});
|
|
@@ -10156,6 +10206,10 @@ var require_websocket_server = __commonJS({
|
|
|
10156
10206
|
* called
|
|
10157
10207
|
* @param {Function} [options.handleProtocols] A hook to handle protocols
|
|
10158
10208
|
* @param {String} [options.host] The hostname where to bind the server
|
|
10209
|
+
* @param {Number} [options.maxBufferedChunks=1048576] The maximum number of
|
|
10210
|
+
* buffered data chunks
|
|
10211
|
+
* @param {Number} [options.maxFragments=131072] The maximum number of message
|
|
10212
|
+
* fragments
|
|
10159
10213
|
* @param {Number} [options.maxPayload=104857600] The maximum allowed message
|
|
10160
10214
|
* size
|
|
10161
10215
|
* @param {Boolean} [options.noServer=false] Enable no server mode
|
|
@@ -10177,6 +10231,8 @@ var require_websocket_server = __commonJS({
|
|
|
10177
10231
|
options = {
|
|
10178
10232
|
allowSynchronousEvents: true,
|
|
10179
10233
|
autoPong: true,
|
|
10234
|
+
maxBufferedChunks: 1024 * 1024,
|
|
10235
|
+
maxFragments: 128 * 1024,
|
|
10180
10236
|
maxPayload: 100 * 1024 * 1024,
|
|
10181
10237
|
skipUTF8Validation: false,
|
|
10182
10238
|
perMessageDeflate: false,
|
|
@@ -10456,6 +10512,8 @@ var require_websocket_server = __commonJS({
|
|
|
10456
10512
|
socket.removeListener("error", socketOnError);
|
|
10457
10513
|
ws.setSocket(socket, head, {
|
|
10458
10514
|
allowSynchronousEvents: this.options.allowSynchronousEvents,
|
|
10515
|
+
maxBufferedChunks: this.options.maxBufferedChunks,
|
|
10516
|
+
maxFragments: this.options.maxFragments,
|
|
10459
10517
|
maxPayload: this.options.maxPayload,
|
|
10460
10518
|
skipUTF8Validation: this.options.skipUTF8Validation
|
|
10461
10519
|
});
|
|
@@ -34582,6 +34640,7 @@ var StdioServerTransport = class {
|
|
|
34582
34640
|
|
|
34583
34641
|
// node_modules/@fetchproxy/protocol/dist/frames.js
|
|
34584
34642
|
var PROTOCOL_VERSION = 2;
|
|
34643
|
+
var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
|
|
34585
34644
|
var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
|
|
34586
34645
|
"fetch",
|
|
34587
34646
|
"read_cookies",
|
|
@@ -34656,7 +34715,7 @@ var ProtocolError = class extends Error {
|
|
|
34656
34715
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
34657
34716
|
var BASE64_RE = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
34658
34717
|
var SCOPE_KEY_RE = /^[A-Za-z0-9_.\-]{1,256}$/;
|
|
34659
|
-
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,
|
|
34718
|
+
var SCOPE_KEY_GLOB_RE = /^[A-Za-z0-9_.\-]{1,256}\*?$/;
|
|
34660
34719
|
var HEADER_NAME_RE = /^[A-Za-z0-9_\-]{1,128}$/;
|
|
34661
34720
|
var HOSTNAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)+$/i;
|
|
34662
34721
|
function assertObject(x, label) {
|
|
@@ -34785,7 +34844,7 @@ function assertStoragePointersArray(value, label, declaredKeys) {
|
|
|
34785
34844
|
if (entry.jsonPointer === void 0) {
|
|
34786
34845
|
throw new ProtocolError(`${label}[${i}].jsonPointer: missing`);
|
|
34787
34846
|
}
|
|
34788
|
-
if (typeof entry.key !== "string" || !
|
|
34847
|
+
if (typeof entry.key !== "string" || !SCOPE_KEY_RE.test(entry.key)) {
|
|
34789
34848
|
throw new ProtocolError(`${label}[${i}].key: invalid key ${JSON.stringify(entry.key)}`);
|
|
34790
34849
|
}
|
|
34791
34850
|
if (typeof entry.jsonPointer !== "string" || !isValidJsonPointer(entry.jsonPointer)) {
|
|
@@ -34884,6 +34943,8 @@ function validateFrame(raw) {
|
|
|
34884
34943
|
return validateReady(raw);
|
|
34885
34944
|
if (t === "frame")
|
|
34886
34945
|
return validateEncrypted(raw);
|
|
34946
|
+
if (t === "pair-pending")
|
|
34947
|
+
return validatePairPending(raw);
|
|
34887
34948
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
34888
34949
|
}
|
|
34889
34950
|
function validateHello(raw) {
|
|
@@ -34985,6 +35046,17 @@ function validateEncrypted(raw) {
|
|
|
34985
35046
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
34986
35047
|
return raw;
|
|
34987
35048
|
}
|
|
35049
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
35050
|
+
function validatePairPending(raw) {
|
|
35051
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
35052
|
+
if (!isValidMcpId(raw.mcpId))
|
|
35053
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
35054
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
35055
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
35056
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
35057
|
+
}
|
|
35058
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
35059
|
+
}
|
|
34988
35060
|
function validateInnerFrame(raw) {
|
|
34989
35061
|
assertObject(raw, "inner");
|
|
34990
35062
|
const t = raw.type;
|
|
@@ -35567,15 +35639,22 @@ async function startHost(opts) {
|
|
|
35567
35639
|
let extensionWs = null;
|
|
35568
35640
|
const peers = /* @__PURE__ */ new Map();
|
|
35569
35641
|
const ownInnerListeners = [];
|
|
35642
|
+
const disconnectListeners = [];
|
|
35643
|
+
const pendingPairListeners = [];
|
|
35570
35644
|
let ownSession = null;
|
|
35645
|
+
let ownPendingPairCode = null;
|
|
35571
35646
|
let resolveOwnSession;
|
|
35572
35647
|
let rejectOwnSession;
|
|
35573
|
-
|
|
35574
|
-
|
|
35575
|
-
|
|
35576
|
-
|
|
35577
|
-
|
|
35578
|
-
|
|
35648
|
+
let ownSessionReady;
|
|
35649
|
+
function resetSessionPromise() {
|
|
35650
|
+
ownSessionReady = new Promise((resolve, reject) => {
|
|
35651
|
+
resolveOwnSession = resolve;
|
|
35652
|
+
rejectOwnSession = reject;
|
|
35653
|
+
});
|
|
35654
|
+
ownSessionReady.catch(() => {
|
|
35655
|
+
});
|
|
35656
|
+
}
|
|
35657
|
+
resetSessionPromise();
|
|
35579
35658
|
let extensionHello = null;
|
|
35580
35659
|
wss.on("connection", (ws) => {
|
|
35581
35660
|
let identified = null;
|
|
@@ -35644,11 +35723,12 @@ async function startHost(opts) {
|
|
|
35644
35723
|
}
|
|
35645
35724
|
const extPub = fromB64(frame.extensionSessionPub);
|
|
35646
35725
|
const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
|
|
35647
|
-
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(
|
|
35648
|
-
if (
|
|
35649
|
-
|
|
35650
|
-
|
|
35651
|
-
|
|
35726
|
+
const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
|
|
35727
|
+
if (extensionWs !== ws)
|
|
35728
|
+
return;
|
|
35729
|
+
ownSession = new SessionState(key);
|
|
35730
|
+
ownPendingPairCode = null;
|
|
35731
|
+
resolveOwnSession(ownSession);
|
|
35652
35732
|
} else {
|
|
35653
35733
|
const slot = peers.get(frame.mcpId);
|
|
35654
35734
|
if (slot)
|
|
@@ -35675,6 +35755,16 @@ async function startHost(opts) {
|
|
|
35675
35755
|
extensionWs.send(JSON.stringify(frame));
|
|
35676
35756
|
}
|
|
35677
35757
|
}
|
|
35758
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
35759
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
35760
|
+
ownPendingPairCode = frame.pairCode;
|
|
35761
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35762
|
+
} else {
|
|
35763
|
+
const slot = peers.get(frame.mcpId);
|
|
35764
|
+
if (slot)
|
|
35765
|
+
slot.ws.send(JSON.stringify(frame));
|
|
35766
|
+
}
|
|
35767
|
+
}
|
|
35678
35768
|
} catch (e) {
|
|
35679
35769
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
35680
35770
|
try {
|
|
@@ -35690,6 +35780,9 @@ async function startHost(opts) {
|
|
|
35690
35780
|
if (!ownSession) {
|
|
35691
35781
|
rejectOwnSession(new Error("extension disconnected before ready"));
|
|
35692
35782
|
}
|
|
35783
|
+
ownSession = null;
|
|
35784
|
+
resetSessionPromise();
|
|
35785
|
+
disconnectListeners.forEach((cb) => cb());
|
|
35693
35786
|
}
|
|
35694
35787
|
if (identified === "peer" && peerMcpId)
|
|
35695
35788
|
peers.delete(peerMcpId);
|
|
@@ -35714,7 +35807,14 @@ async function startHost(opts) {
|
|
|
35714
35807
|
},
|
|
35715
35808
|
onOwnInner: (cb) => {
|
|
35716
35809
|
ownInnerListeners.push(cb);
|
|
35717
|
-
}
|
|
35810
|
+
},
|
|
35811
|
+
onExtensionDisconnect: (cb) => {
|
|
35812
|
+
disconnectListeners.push(cb);
|
|
35813
|
+
},
|
|
35814
|
+
onPendingPair: (cb) => {
|
|
35815
|
+
pendingPairListeners.push(cb);
|
|
35816
|
+
},
|
|
35817
|
+
pendingPairCode: () => ownPendingPairCode
|
|
35718
35818
|
};
|
|
35719
35819
|
}
|
|
35720
35820
|
|
|
@@ -35744,36 +35844,57 @@ async function startPeer(opts) {
|
|
|
35744
35844
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
35745
35845
|
ws.send(JSON.stringify(hello));
|
|
35746
35846
|
const innerListeners = [];
|
|
35847
|
+
const renegotiateListeners = [];
|
|
35848
|
+
const pendingPairListeners = [];
|
|
35747
35849
|
let session = null;
|
|
35850
|
+
let pendingPairCode = null;
|
|
35851
|
+
let resolveFirstReady;
|
|
35852
|
+
let rejectFirstReady;
|
|
35748
35853
|
const sessionPromise = new Promise((resolve, reject) => {
|
|
35749
|
-
|
|
35750
|
-
|
|
35751
|
-
|
|
35752
|
-
|
|
35753
|
-
|
|
35754
|
-
|
|
35755
|
-
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35854
|
+
resolveFirstReady = resolve;
|
|
35855
|
+
rejectFirstReady = reject;
|
|
35856
|
+
});
|
|
35857
|
+
const onMessage = async (data) => {
|
|
35858
|
+
try {
|
|
35859
|
+
const raw = JSON.parse(data.toString());
|
|
35860
|
+
const frame = validateFrame(raw);
|
|
35861
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
35862
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
35863
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
35864
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
35865
|
+
const isRenegotiation = session !== null;
|
|
35866
|
+
session = new SessionState(sessionKey);
|
|
35867
|
+
pendingPairCode = null;
|
|
35868
|
+
if (isRenegotiation) {
|
|
35869
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
35870
|
+
} else {
|
|
35871
|
+
resolveFirstReady(session);
|
|
35760
35872
|
}
|
|
35761
|
-
|
|
35762
|
-
|
|
35763
|
-
|
|
35764
|
-
|
|
35765
|
-
|
|
35873
|
+
return;
|
|
35874
|
+
}
|
|
35875
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
35876
|
+
pendingPairCode = frame.pairCode;
|
|
35877
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35878
|
+
return;
|
|
35879
|
+
}
|
|
35880
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
35881
|
+
if (!session)
|
|
35882
|
+
return;
|
|
35883
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
35884
|
+
return;
|
|
35885
|
+
try {
|
|
35766
35886
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
35767
35887
|
innerListeners.forEach((cb) => cb(inner));
|
|
35888
|
+
} catch {
|
|
35768
35889
|
}
|
|
35769
|
-
} catch (e) {
|
|
35770
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
35771
35890
|
}
|
|
35772
|
-
}
|
|
35773
|
-
|
|
35774
|
-
|
|
35775
|
-
|
|
35776
|
-
|
|
35891
|
+
} catch (e) {
|
|
35892
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
35893
|
+
}
|
|
35894
|
+
};
|
|
35895
|
+
ws.on("message", onMessage);
|
|
35896
|
+
ws.once("close", () => {
|
|
35897
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
35777
35898
|
});
|
|
35778
35899
|
sessionPromise.catch(() => {
|
|
35779
35900
|
});
|
|
@@ -35781,13 +35902,21 @@ async function startPeer(opts) {
|
|
|
35781
35902
|
ws,
|
|
35782
35903
|
session: sessionPromise,
|
|
35783
35904
|
sendInner: async (inner) => {
|
|
35784
|
-
|
|
35905
|
+
await sessionPromise;
|
|
35906
|
+
const s = session;
|
|
35785
35907
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
35786
35908
|
ws.send(JSON.stringify(sealed));
|
|
35787
35909
|
},
|
|
35788
35910
|
onInner: (cb) => {
|
|
35789
35911
|
innerListeners.push(cb);
|
|
35790
35912
|
},
|
|
35913
|
+
onRenegotiate: (cb) => {
|
|
35914
|
+
renegotiateListeners.push(cb);
|
|
35915
|
+
},
|
|
35916
|
+
onPendingPair: (cb) => {
|
|
35917
|
+
pendingPairListeners.push(cb);
|
|
35918
|
+
},
|
|
35919
|
+
pendingPairCode: () => pendingPairCode,
|
|
35791
35920
|
close: () => ws.close()
|
|
35792
35921
|
};
|
|
35793
35922
|
return handle;
|
|
@@ -35844,6 +35973,32 @@ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
|
|
|
35844
35973
|
return id;
|
|
35845
35974
|
}
|
|
35846
35975
|
|
|
35976
|
+
// node_modules/@fetchproxy/server/dist/error-kind.js
|
|
35977
|
+
function classifyFetchError(error51) {
|
|
35978
|
+
if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
|
|
35979
|
+
return "content_script_unreachable";
|
|
35980
|
+
}
|
|
35981
|
+
if (/^tab fetch failed:/.test(error51)) {
|
|
35982
|
+
return "tab_fetch_failed";
|
|
35983
|
+
}
|
|
35984
|
+
if (/^fetch threw:/.test(error51)) {
|
|
35985
|
+
return "tab_fetch_failed";
|
|
35986
|
+
}
|
|
35987
|
+
if (/^no tab matching /.test(error51)) {
|
|
35988
|
+
return "no_tab";
|
|
35989
|
+
}
|
|
35990
|
+
if (/not in domains \[/.test(error51)) {
|
|
35991
|
+
return "domain_denied";
|
|
35992
|
+
}
|
|
35993
|
+
if (/^capability .+ not granted/.test(error51)) {
|
|
35994
|
+
return "capability_denied";
|
|
35995
|
+
}
|
|
35996
|
+
if (/^(request|response) body too large:/.test(error51)) {
|
|
35997
|
+
return "body_too_large";
|
|
35998
|
+
}
|
|
35999
|
+
return "other";
|
|
36000
|
+
}
|
|
36001
|
+
|
|
35847
36002
|
// node_modules/@fetchproxy/server/dist/ws-server.js
|
|
35848
36003
|
var FetchproxyProtocolError = class extends Error {
|
|
35849
36004
|
constructor(message) {
|
|
@@ -35886,7 +36041,11 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
35886
36041
|
}
|
|
35887
36042
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
35888
36043
|
var FetchproxyServer = class {
|
|
35889
|
-
/**
|
|
36044
|
+
/**
|
|
36045
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
36046
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
36047
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
36048
|
+
*/
|
|
35890
36049
|
role = null;
|
|
35891
36050
|
opts;
|
|
35892
36051
|
hostHandle = null;
|
|
@@ -35908,6 +36067,12 @@ var FetchproxyServer = class {
|
|
|
35908
36067
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
35909
36068
|
mcpId = null;
|
|
35910
36069
|
identity = null;
|
|
36070
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
36071
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
36072
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
36073
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
36074
|
+
// race the port bind.
|
|
36075
|
+
connectingPromise = null;
|
|
35911
36076
|
constructor(opts) {
|
|
35912
36077
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
35913
36078
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -35959,23 +36124,87 @@ var FetchproxyServer = class {
|
|
|
35959
36124
|
};
|
|
35960
36125
|
}
|
|
35961
36126
|
/**
|
|
35962
|
-
*
|
|
35963
|
-
* from disk (creating it on first call)
|
|
35964
|
-
*
|
|
35965
|
-
*
|
|
35966
|
-
* `
|
|
35967
|
-
*
|
|
36127
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36128
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36129
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36130
|
+
* connection is established lazily on the first verb call (see
|
|
36131
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36132
|
+
*
|
|
36133
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36134
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36135
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36136
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36137
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36138
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36139
|
+
* leaves the port unowned until something actually needs it.
|
|
36140
|
+
*
|
|
36141
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36142
|
+
* no-op (the second call's identity load is idempotent).
|
|
35968
36143
|
*/
|
|
35969
36144
|
async listen() {
|
|
35970
|
-
|
|
35971
|
-
|
|
36145
|
+
if (!this.identity) {
|
|
36146
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36147
|
+
}
|
|
36148
|
+
if (!this.mcpId) {
|
|
36149
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36150
|
+
}
|
|
36151
|
+
}
|
|
36152
|
+
/**
|
|
36153
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36154
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36155
|
+
* Useful for callers that want to surface the role / connection
|
|
36156
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36157
|
+
* immediately after server construction. Production MCPs that just
|
|
36158
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36159
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36160
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36161
|
+
* actually get invoked.
|
|
36162
|
+
*
|
|
36163
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36164
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36165
|
+
* called.
|
|
36166
|
+
*/
|
|
36167
|
+
async connect() {
|
|
36168
|
+
await this.ensureConnected();
|
|
36169
|
+
}
|
|
36170
|
+
/**
|
|
36171
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36172
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36173
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36174
|
+
* share the same in-flight promise so only one election happens.
|
|
36175
|
+
*
|
|
36176
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36177
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36178
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36179
|
+
* verb runs.
|
|
36180
|
+
*/
|
|
36181
|
+
async ensureConnected() {
|
|
36182
|
+
if (this.hostHandle || this.peerHandle)
|
|
36183
|
+
return;
|
|
36184
|
+
if (this.connectingPromise) {
|
|
36185
|
+
await this.connectingPromise;
|
|
36186
|
+
return;
|
|
36187
|
+
}
|
|
36188
|
+
if (!this.identity || !this.mcpId) {
|
|
36189
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36190
|
+
}
|
|
36191
|
+
this.connectingPromise = this.doConnect();
|
|
36192
|
+
try {
|
|
36193
|
+
await this.connectingPromise;
|
|
36194
|
+
} finally {
|
|
36195
|
+
this.connectingPromise = null;
|
|
36196
|
+
}
|
|
36197
|
+
}
|
|
36198
|
+
async doConnect() {
|
|
36199
|
+
const identity = this.identity;
|
|
36200
|
+
const mcpId = this.mcpId;
|
|
35972
36201
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
35973
36202
|
if (el.role === "host") {
|
|
35974
36203
|
this.role = "host";
|
|
35975
36204
|
this.hostHandle = await startHost({
|
|
35976
36205
|
httpServer: el.server,
|
|
35977
|
-
ownIdentity:
|
|
35978
|
-
ownMcpId:
|
|
36206
|
+
ownIdentity: identity,
|
|
36207
|
+
ownMcpId: mcpId,
|
|
35979
36208
|
ownServerName: this.opts.serverName,
|
|
35980
36209
|
ownVersion: this.opts.version,
|
|
35981
36210
|
ownDomains: this.opts.domains,
|
|
@@ -35990,13 +36219,17 @@ var FetchproxyServer = class {
|
|
|
35990
36219
|
onPairCode: this.opts.onPairCode
|
|
35991
36220
|
});
|
|
35992
36221
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36222
|
+
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36223
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36224
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36225
|
+
});
|
|
35993
36226
|
} else {
|
|
35994
36227
|
this.role = "peer";
|
|
35995
36228
|
this.peerHandle = await startPeer({
|
|
35996
36229
|
host: this.opts.host,
|
|
35997
36230
|
port: this.opts.port,
|
|
35998
|
-
identity
|
|
35999
|
-
mcpId
|
|
36231
|
+
identity,
|
|
36232
|
+
mcpId,
|
|
36000
36233
|
serverName: this.opts.serverName,
|
|
36001
36234
|
version: this.opts.version,
|
|
36002
36235
|
domains: this.opts.domains,
|
|
@@ -36010,8 +36243,19 @@ var FetchproxyServer = class {
|
|
|
36010
36243
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36011
36244
|
});
|
|
36012
36245
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36246
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36247
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36248
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36249
|
+
});
|
|
36250
|
+
if (this.opts.onPairCode) {
|
|
36251
|
+
const cb = this.opts.onPairCode;
|
|
36252
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36253
|
+
}
|
|
36013
36254
|
}
|
|
36014
36255
|
}
|
|
36256
|
+
pairingErrorMessage(code) {
|
|
36257
|
+
return `fetchproxy: pairing required for ${this.opts.serverName} \u2014 open the fetchproxy extension popup in Chrome and approve the pair request. Verify the pair code matches: ${code}`;
|
|
36258
|
+
}
|
|
36015
36259
|
/**
|
|
36016
36260
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36017
36261
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36027,8 +36271,11 @@ var FetchproxyServer = class {
|
|
|
36027
36271
|
* offline, etc.).
|
|
36028
36272
|
*/
|
|
36029
36273
|
async fetch(init) {
|
|
36030
|
-
|
|
36031
|
-
|
|
36274
|
+
await this.ensureConnected();
|
|
36275
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36276
|
+
if (pendingCode !== null) {
|
|
36277
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36278
|
+
return { ok: false, error: error51, kind: classifyFetchError(error51) };
|
|
36032
36279
|
}
|
|
36033
36280
|
const id = this.nextRequestId++;
|
|
36034
36281
|
const inner = { type: "request", id, op: "fetch", init };
|
|
@@ -36169,9 +36416,8 @@ var FetchproxyServer = class {
|
|
|
36169
36416
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36170
36417
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36171
36418
|
}
|
|
36172
|
-
|
|
36173
|
-
|
|
36174
|
-
}
|
|
36419
|
+
await this.ensureConnected();
|
|
36420
|
+
this.throwIfPendingPair();
|
|
36175
36421
|
if (opts.subdomain !== void 0)
|
|
36176
36422
|
assertSubdomainLabel(opts.subdomain);
|
|
36177
36423
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36228,9 +36474,8 @@ var FetchproxyServer = class {
|
|
|
36228
36474
|
if (!this.opts.capabilities.includes(op)) {
|
|
36229
36475
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36230
36476
|
}
|
|
36231
|
-
|
|
36232
|
-
|
|
36233
|
-
}
|
|
36477
|
+
await this.ensureConnected();
|
|
36478
|
+
this.throwIfPendingPair();
|
|
36234
36479
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36235
36480
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36236
36481
|
}
|
|
@@ -36285,9 +36530,8 @@ var FetchproxyServer = class {
|
|
|
36285
36530
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36286
36531
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36287
36532
|
}
|
|
36288
|
-
|
|
36289
|
-
|
|
36290
|
-
}
|
|
36533
|
+
await this.ensureConnected();
|
|
36534
|
+
this.throwIfPendingPair();
|
|
36291
36535
|
const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
36292
36536
|
if (!declared) {
|
|
36293
36537
|
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
@@ -36328,9 +36572,8 @@ var FetchproxyServer = class {
|
|
|
36328
36572
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36329
36573
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36330
36574
|
}
|
|
36331
|
-
|
|
36332
|
-
|
|
36333
|
-
}
|
|
36575
|
+
await this.ensureConnected();
|
|
36576
|
+
this.throwIfPendingPair();
|
|
36334
36577
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36335
36578
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36336
36579
|
}
|
|
@@ -36408,10 +36651,11 @@ var FetchproxyServer = class {
|
|
|
36408
36651
|
if (inner.op === void 0 || inner.op === "fetch") {
|
|
36409
36652
|
fetchCb({ ok: true, status: inner.status, url: inner.url, body: inner.body });
|
|
36410
36653
|
} else {
|
|
36411
|
-
|
|
36654
|
+
const error51 = `unexpected ${inner.op} response on fetch awaiter`;
|
|
36655
|
+
fetchCb({ ok: false, error: error51, kind: classifyFetchError(error51) });
|
|
36412
36656
|
}
|
|
36413
36657
|
} else {
|
|
36414
|
-
fetchCb({ ok: false, error: inner.error });
|
|
36658
|
+
fetchCb({ ok: false, error: inner.error, kind: classifyFetchError(inner.error) });
|
|
36415
36659
|
}
|
|
36416
36660
|
return;
|
|
36417
36661
|
}
|
|
@@ -36478,6 +36722,52 @@ var FetchproxyServer = class {
|
|
|
36478
36722
|
}
|
|
36479
36723
|
}
|
|
36480
36724
|
}
|
|
36725
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
36726
|
+
const err = new FetchproxyProtocolError(reason);
|
|
36727
|
+
for (const cb of this.pending.values()) {
|
|
36728
|
+
cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
|
|
36729
|
+
}
|
|
36730
|
+
this.pending.clear();
|
|
36731
|
+
for (const cb of this.pendingReadCookies.values()) {
|
|
36732
|
+
cb({ ok: false, error: err.message });
|
|
36733
|
+
}
|
|
36734
|
+
this.pendingReadCookies.clear();
|
|
36735
|
+
for (const { reject } of this.pendingStorage.values())
|
|
36736
|
+
reject(err);
|
|
36737
|
+
this.pendingStorage.clear();
|
|
36738
|
+
for (const { reject } of this.pendingCapture.values())
|
|
36739
|
+
reject(err);
|
|
36740
|
+
this.pendingCapture.clear();
|
|
36741
|
+
for (const { reject } of this.pendingIdb.values())
|
|
36742
|
+
reject(err);
|
|
36743
|
+
this.pendingIdb.clear();
|
|
36744
|
+
}
|
|
36745
|
+
/**
|
|
36746
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
36747
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
36748
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
36749
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
36750
|
+
* sealed frame the extension will never process.
|
|
36751
|
+
*/
|
|
36752
|
+
currentPendingPairCode() {
|
|
36753
|
+
if (this.hostHandle)
|
|
36754
|
+
return this.hostHandle.pendingPairCode();
|
|
36755
|
+
if (this.peerHandle)
|
|
36756
|
+
return this.peerHandle.pendingPairCode();
|
|
36757
|
+
return null;
|
|
36758
|
+
}
|
|
36759
|
+
/**
|
|
36760
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
36761
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
36762
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
36763
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
36764
|
+
*/
|
|
36765
|
+
throwIfPendingPair() {
|
|
36766
|
+
const code = this.currentPendingPairCode();
|
|
36767
|
+
if (code !== null) {
|
|
36768
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
36769
|
+
}
|
|
36770
|
+
}
|
|
36481
36771
|
/**
|
|
36482
36772
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36483
36773
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36485,6 +36775,10 @@ var FetchproxyServer = class {
|
|
|
36485
36775
|
* twice in a row.
|
|
36486
36776
|
*/
|
|
36487
36777
|
async close() {
|
|
36778
|
+
this.rejectAllPending();
|
|
36779
|
+
if (this.connectingPromise) {
|
|
36780
|
+
await this.connectingPromise.catch(() => void 0);
|
|
36781
|
+
}
|
|
36488
36782
|
if (this.hostHandle)
|
|
36489
36783
|
await this.hostHandle.close();
|
|
36490
36784
|
if (this.peerHandle)
|
|
@@ -36492,6 +36786,7 @@ var FetchproxyServer = class {
|
|
|
36492
36786
|
this.hostHandle = null;
|
|
36493
36787
|
this.peerHandle = null;
|
|
36494
36788
|
this.role = null;
|
|
36789
|
+
this.connectingPromise = null;
|
|
36495
36790
|
}
|
|
36496
36791
|
};
|
|
36497
36792
|
|
|
@@ -36580,8 +36875,7 @@ async function bootstrap(opts) {
|
|
|
36580
36875
|
for (const p of localStoragePointers) {
|
|
36581
36876
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36582
36877
|
}
|
|
36583
|
-
|
|
36584
|
-
localStorage = await stub.readLocalStorage({
|
|
36878
|
+
localStorage = await server2.readLocalStorage({
|
|
36585
36879
|
keys: allKeys,
|
|
36586
36880
|
...storageDomainOpts,
|
|
36587
36881
|
...localStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -36594,8 +36888,7 @@ async function bootstrap(opts) {
|
|
|
36594
36888
|
for (const p of sessionStoragePointers) {
|
|
36595
36889
|
pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
|
|
36596
36890
|
}
|
|
36597
|
-
|
|
36598
|
-
sessionStorage = await stub.readSessionStorage({
|
|
36891
|
+
sessionStorage = await server2.readSessionStorage({
|
|
36599
36892
|
keys: allKeys,
|
|
36600
36893
|
...storageDomainOpts,
|
|
36601
36894
|
...sessionStoragePointers.length > 0 ? { pointers } : {}
|
|
@@ -36618,9 +36911,6 @@ async function bootstrap(opts) {
|
|
|
36618
36911
|
}
|
|
36619
36912
|
const indexedDbBucket = {};
|
|
36620
36913
|
for (const d of indexedDb) {
|
|
36621
|
-
if (!server2.readIndexedDb) {
|
|
36622
|
-
throw new Error("bootstrap: server factory does not implement readIndexedDb (declared indexedDb but server stub omits it)");
|
|
36623
|
-
}
|
|
36624
36914
|
const values = await server2.readIndexedDb({
|
|
36625
36915
|
database: d.database,
|
|
36626
36916
|
store: d.store,
|
|
@@ -36733,7 +37023,7 @@ function loadAccount(env = process.env) {
|
|
|
36733
37023
|
// package.json
|
|
36734
37024
|
var package_default = {
|
|
36735
37025
|
name: "signupgenius-mcp",
|
|
36736
|
-
version: "1.0.
|
|
37026
|
+
version: "1.0.7",
|
|
36737
37027
|
mcpName: "io.github.chrischall/signupgenius-mcp",
|
|
36738
37028
|
description: "SignUpGenius MCP server \u2014 read sign-ups, reports, and groups; add group members.",
|
|
36739
37029
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36770,17 +37060,17 @@ var package_default = {
|
|
|
36770
37060
|
"test:watch": "vitest"
|
|
36771
37061
|
},
|
|
36772
37062
|
dependencies: {
|
|
36773
|
-
"@fetchproxy/bootstrap": "^0.
|
|
37063
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
36774
37064
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36775
|
-
dotenv: "^17.4.
|
|
36776
|
-
zod: "^4.3
|
|
37065
|
+
dotenv: "^17.4.2",
|
|
37066
|
+
zod: "^4.4.3"
|
|
36777
37067
|
},
|
|
36778
37068
|
devDependencies: {
|
|
36779
|
-
"@types/node": "^25.
|
|
36780
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37069
|
+
"@types/node": "^25.9.1",
|
|
37070
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
36781
37071
|
esbuild: "^0.28.0",
|
|
36782
|
-
typescript: "^6.0.
|
|
36783
|
-
vitest: "^4.1.
|
|
37072
|
+
typescript: "^6.0.3",
|
|
37073
|
+
vitest: "^4.1.7"
|
|
36784
37074
|
}
|
|
36785
37075
|
};
|
|
36786
37076
|
|
|
@@ -37575,23 +37865,39 @@ function buildRsvpPayload(parts, info, input) {
|
|
|
37575
37865
|
const adults = isNo ? 0 : input.adults ?? 1;
|
|
37576
37866
|
const children = isNo ? 0 : input.children ?? 0;
|
|
37577
37867
|
return {
|
|
37578
|
-
|
|
37579
|
-
|
|
37868
|
+
listid: info.id,
|
|
37869
|
+
owner: info.owner,
|
|
37580
37870
|
urlid: parts.urlid,
|
|
37581
|
-
|
|
37582
|
-
|
|
37583
|
-
|
|
37584
|
-
|
|
37585
|
-
|
|
37871
|
+
title: info.title,
|
|
37872
|
+
siid: "",
|
|
37873
|
+
rsvpid: 0,
|
|
37874
|
+
imid: 0,
|
|
37875
|
+
usealternatename: false,
|
|
37876
|
+
changemembermame: false,
|
|
37877
|
+
displayfirstname: input.firstname,
|
|
37878
|
+
displaylastname: input.lastname,
|
|
37586
37879
|
firstname: input.firstname,
|
|
37587
37880
|
lastname: input.lastname,
|
|
37588
37881
|
email: input.email,
|
|
37589
|
-
|
|
37882
|
+
optInStatus: false,
|
|
37883
|
+
savecontactinfo: false,
|
|
37884
|
+
rsvpresponse: letter,
|
|
37885
|
+
rsvpadult: adults,
|
|
37886
|
+
rsvpchildren: children,
|
|
37887
|
+
rsvpitems: [],
|
|
37888
|
+
rsvpcomments: input.comment ?? "",
|
|
37889
|
+
type: "rsvp",
|
|
37890
|
+
source: "main",
|
|
37891
|
+
slotid: info.rsvpdetails.slotid,
|
|
37590
37892
|
isLoggedin: true,
|
|
37591
37893
|
payLater: false,
|
|
37592
37894
|
customFields: []
|
|
37593
37895
|
};
|
|
37594
37896
|
}
|
|
37897
|
+
function isItemBasedRsvp(info) {
|
|
37898
|
+
const items = info.rsvpdetails.rsvpitems;
|
|
37899
|
+
return Array.isArray(items) && items.length > 0;
|
|
37900
|
+
}
|
|
37595
37901
|
function registerRsvpTool(server2, client2) {
|
|
37596
37902
|
if (client2.mode !== "session") return;
|
|
37597
37903
|
server2.registerTool(
|
|
@@ -37615,6 +37921,12 @@ function registerRsvpTool(server2, client2) {
|
|
|
37615
37921
|
`Sign-up ${parts.urlid} is not an RSVP-style sheet (useRSVP=${info.useRSVP}). This tool only handles Yes/No/Maybe responses. Slot-based sign-ups need a separate tool.`
|
|
37616
37922
|
);
|
|
37617
37923
|
}
|
|
37924
|
+
if (isItemBasedRsvp(info)) {
|
|
37925
|
+
const itemCount = info.rsvpdetails.rsvpitems.length;
|
|
37926
|
+
throw new Error(
|
|
37927
|
+
`Sign-up ${parts.urlid} is an item-based RSVP \u2014 guests must pick from ${itemCount} item slot(s) (e.g. "Yes, I'll bring lasagna"). This tool only handles the headcount-only RSVP variant. Use the SignUpGenius web UI for item-based responses until a dedicated tool ships.`
|
|
37928
|
+
);
|
|
37929
|
+
}
|
|
37618
37930
|
const payload = buildRsvpPayload(parts, info, args);
|
|
37619
37931
|
const result = await client2.request("", {
|
|
37620
37932
|
legacyAction: "s.processSignUpFormHandler",
|
|
@@ -37632,7 +37944,7 @@ function registerRsvpTool(server2, client2) {
|
|
|
37632
37944
|
response: args.response,
|
|
37633
37945
|
adults: payload.rsvpadult,
|
|
37634
37946
|
children: payload.rsvpchildren,
|
|
37635
|
-
comment: payload.
|
|
37947
|
+
comment: payload.rsvpcomments,
|
|
37636
37948
|
server: result.data
|
|
37637
37949
|
});
|
|
37638
37950
|
}
|
|
@@ -37662,7 +37974,7 @@ var client = new SignUpGeniusClient(account, {
|
|
|
37662
37974
|
configError: configError ?? void 0,
|
|
37663
37975
|
preloaded
|
|
37664
37976
|
});
|
|
37665
|
-
var server = new McpServer({ name: "signupgenius", version: "1.0.
|
|
37977
|
+
var server = new McpServer({ name: "signupgenius", version: "1.0.7" });
|
|
37666
37978
|
registerUserTools(server, client);
|
|
37667
37979
|
registerGroupTools(server, client);
|
|
37668
37980
|
registerSignUpTools(server, client);
|
package/dist/index.js
CHANGED
|
@@ -43,7 +43,7 @@ const client = new SignUpGeniusClient(account, {
|
|
|
43
43
|
configError: configError ?? undefined,
|
|
44
44
|
preloaded,
|
|
45
45
|
});
|
|
46
|
-
const server = new McpServer({ name: 'signupgenius', version: '1.0.
|
|
46
|
+
const server = new McpServer({ name: 'signupgenius', version: '1.0.7' }); // x-release-please-version
|
|
47
47
|
registerUserTools(server, client);
|
|
48
48
|
registerGroupTools(server, client);
|
|
49
49
|
registerSignUpTools(server, client);
|
package/dist/tools/rsvp.js
CHANGED
|
@@ -43,23 +43,46 @@ export function buildRsvpPayload(parts, info, input) {
|
|
|
43
43
|
const adults = isNo ? 0 : input.adults ?? 1;
|
|
44
44
|
const children = isNo ? 0 : input.children ?? 0;
|
|
45
45
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
listid: info.id,
|
|
47
|
+
owner: info.owner,
|
|
48
48
|
urlid: parts.urlid,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
title: info.title,
|
|
50
|
+
siid: '',
|
|
51
|
+
rsvpid: 0,
|
|
52
|
+
imid: 0,
|
|
53
|
+
usealternatename: false,
|
|
54
|
+
changemembermame: false,
|
|
55
|
+
displayfirstname: input.firstname,
|
|
56
|
+
displaylastname: input.lastname,
|
|
54
57
|
firstname: input.firstname,
|
|
55
58
|
lastname: input.lastname,
|
|
56
59
|
email: input.email,
|
|
57
|
-
|
|
60
|
+
optInStatus: false,
|
|
61
|
+
savecontactinfo: false,
|
|
62
|
+
rsvpresponse: letter,
|
|
63
|
+
rsvpadult: adults,
|
|
64
|
+
rsvpchildren: children,
|
|
65
|
+
rsvpitems: [],
|
|
66
|
+
rsvpcomments: input.comment ?? '',
|
|
67
|
+
type: 'rsvp',
|
|
68
|
+
source: 'main',
|
|
69
|
+
slotid: info.rsvpdetails.slotid,
|
|
58
70
|
isLoggedin: true,
|
|
59
71
|
payLater: false,
|
|
60
72
|
customFields: [],
|
|
61
73
|
};
|
|
62
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* True when `getSignupInfo` reports per-item slots on an RSVP-style sheet
|
|
77
|
+
* (e.g. "Yes, I'll bring lasagna"). Headcount-only RSVPs leave `rsvpitems`
|
|
78
|
+
* empty or unset. We only handle the headcount variant — the item variant
|
|
79
|
+
* needs a separate input surface (per-slot quantities + comments) that this
|
|
80
|
+
* tool doesn't expose.
|
|
81
|
+
*/
|
|
82
|
+
export function isItemBasedRsvp(info) {
|
|
83
|
+
const items = info.rsvpdetails.rsvpitems;
|
|
84
|
+
return Array.isArray(items) && items.length > 0;
|
|
85
|
+
}
|
|
63
86
|
export function registerRsvpTool(server, client) {
|
|
64
87
|
// Key mode doesn't have the cookie/JWT surface this flow needs, and the
|
|
65
88
|
// documented Pro API has no equivalent. Skip registration entirely so the
|
|
@@ -88,6 +111,15 @@ export function registerRsvpTool(server, client) {
|
|
|
88
111
|
throw new Error(`Sign-up ${parts.urlid} is not an RSVP-style sheet (useRSVP=${info.useRSVP}). ` +
|
|
89
112
|
'This tool only handles Yes/No/Maybe responses. Slot-based sign-ups need a separate tool.');
|
|
90
113
|
}
|
|
114
|
+
if (isItemBasedRsvp(info)) {
|
|
115
|
+
// Length is safe to read because isItemBasedRsvp confirmed the array.
|
|
116
|
+
const itemCount = info.rsvpdetails.rsvpitems.length;
|
|
117
|
+
throw new Error(`Sign-up ${parts.urlid} is an item-based RSVP — guests must pick from ` +
|
|
118
|
+
`${itemCount} item slot(s) ` +
|
|
119
|
+
"(e.g. \"Yes, I'll bring lasagna\"). This tool only handles the " +
|
|
120
|
+
'headcount-only RSVP variant. Use the SignUpGenius web UI for ' +
|
|
121
|
+
'item-based responses until a dedicated tool ships.');
|
|
122
|
+
}
|
|
91
123
|
const payload = buildRsvpPayload(parts, info, args);
|
|
92
124
|
const result = await client.request('', {
|
|
93
125
|
legacyAction: 's.processSignUpFormHandler',
|
|
@@ -105,7 +137,7 @@ export function registerRsvpTool(server, client) {
|
|
|
105
137
|
response: args.response,
|
|
106
138
|
adults: payload.rsvpadult,
|
|
107
139
|
children: payload.rsvpchildren,
|
|
108
|
-
comment: payload.
|
|
140
|
+
comment: payload.rsvpcomments,
|
|
109
141
|
server: result.data,
|
|
110
142
|
});
|
|
111
143
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signupgenius-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"mcpName": "io.github.chrischall/signupgenius-mcp",
|
|
5
5
|
"description": "SignUpGenius MCP server — read sign-ups, reports, and groups; add group members.",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"test:watch": "vitest"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@fetchproxy/bootstrap": "^0.
|
|
40
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
42
|
-
"dotenv": "^17.4.
|
|
43
|
-
"zod": "^4.3
|
|
42
|
+
"dotenv": "^17.4.2",
|
|
43
|
+
"zod": "^4.4.3"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@types/node": "^25.
|
|
47
|
-
"@vitest/coverage-v8": "^4.1.
|
|
46
|
+
"@types/node": "^25.9.1",
|
|
47
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
48
48
|
"esbuild": "^0.28.0",
|
|
49
|
-
"typescript": "^6.0.
|
|
50
|
-
"vitest": "^4.1.
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"vitest": "^4.1.7"
|
|
51
51
|
}
|
|
52
52
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/signupgenius-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.7",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "signupgenius-mcp",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.7",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|