canvas-parent-mcp 1.1.2 → 1.1.3
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 +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bundle.js +239 -63
- package/dist/index.js +1 -1
- package/package.json +8 -8
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "MCP server for Canvas LMS (Instructure) — courses, grades, assignments, announcements, planner, conversations, files",
|
|
10
|
-
"version": "1.1.
|
|
10
|
+
"version": "1.1.3"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "Canvas LMS",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Canvas LMS (Instructure) MCP server for Claude — student/observer access via natural language",
|
|
18
|
-
"version": "1.1.
|
|
18
|
+
"version": "1.1.3",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
3
|
"displayName": "Canvas LMS",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.3",
|
|
5
5
|
"description": "Canvas LMS (Instructure) MCP server for Claude — courses, grades, assignments, announcements, planner, conversations, files",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/bundle.js
CHANGED
|
@@ -34856,6 +34856,8 @@ function validateFrame(raw) {
|
|
|
34856
34856
|
return validateReady(raw);
|
|
34857
34857
|
if (t === "frame")
|
|
34858
34858
|
return validateEncrypted(raw);
|
|
34859
|
+
if (t === "pair-pending")
|
|
34860
|
+
return validatePairPending(raw);
|
|
34859
34861
|
throw new ProtocolError(`unknown frame type: ${String(t)}`);
|
|
34860
34862
|
}
|
|
34861
34863
|
function validateHello(raw) {
|
|
@@ -34957,6 +34959,17 @@ function validateEncrypted(raw) {
|
|
|
34957
34959
|
assertBase64(raw.ciphertext, "frame.ciphertext");
|
|
34958
34960
|
return raw;
|
|
34959
34961
|
}
|
|
34962
|
+
var PAIR_CODE_RE = /^\d{3}-\d{3}$/;
|
|
34963
|
+
function validatePairPending(raw) {
|
|
34964
|
+
assertString(raw.mcpId, "pair-pending.mcpId");
|
|
34965
|
+
if (!isValidMcpId(raw.mcpId))
|
|
34966
|
+
throw new ProtocolError("pair-pending.mcpId: invalid format");
|
|
34967
|
+
assertString(raw.pairCode, "pair-pending.pairCode");
|
|
34968
|
+
if (!PAIR_CODE_RE.test(raw.pairCode)) {
|
|
34969
|
+
throw new ProtocolError(`pair-pending.pairCode: must match XXX-XXX, got ${String(raw.pairCode)}`);
|
|
34970
|
+
}
|
|
34971
|
+
return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
|
|
34972
|
+
}
|
|
34960
34973
|
function validateInnerFrame(raw) {
|
|
34961
34974
|
assertObject(raw, "inner");
|
|
34962
34975
|
const t = raw.type;
|
|
@@ -35540,7 +35553,9 @@ async function startHost(opts) {
|
|
|
35540
35553
|
const peers = /* @__PURE__ */ new Map();
|
|
35541
35554
|
const ownInnerListeners = [];
|
|
35542
35555
|
const disconnectListeners = [];
|
|
35556
|
+
const pendingPairListeners = [];
|
|
35543
35557
|
let ownSession = null;
|
|
35558
|
+
let ownPendingPairCode = null;
|
|
35544
35559
|
let resolveOwnSession;
|
|
35545
35560
|
let rejectOwnSession;
|
|
35546
35561
|
let ownSessionReady;
|
|
@@ -35625,6 +35640,7 @@ async function startHost(opts) {
|
|
|
35625
35640
|
if (extensionWs !== ws)
|
|
35626
35641
|
return;
|
|
35627
35642
|
ownSession = new SessionState(key);
|
|
35643
|
+
ownPendingPairCode = null;
|
|
35628
35644
|
resolveOwnSession(ownSession);
|
|
35629
35645
|
} else {
|
|
35630
35646
|
const slot = peers.get(frame.mcpId);
|
|
@@ -35652,6 +35668,16 @@ async function startHost(opts) {
|
|
|
35652
35668
|
extensionWs.send(JSON.stringify(frame));
|
|
35653
35669
|
}
|
|
35654
35670
|
}
|
|
35671
|
+
if (frame.type === "pair-pending" && identified === "extension") {
|
|
35672
|
+
if (frame.mcpId === opts.ownMcpId) {
|
|
35673
|
+
ownPendingPairCode = frame.pairCode;
|
|
35674
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35675
|
+
} else {
|
|
35676
|
+
const slot = peers.get(frame.mcpId);
|
|
35677
|
+
if (slot)
|
|
35678
|
+
slot.ws.send(JSON.stringify(frame));
|
|
35679
|
+
}
|
|
35680
|
+
}
|
|
35655
35681
|
} catch (e) {
|
|
35656
35682
|
console.error("[fetchproxy] host: message handler error:", e);
|
|
35657
35683
|
try {
|
|
@@ -35697,7 +35723,11 @@ async function startHost(opts) {
|
|
|
35697
35723
|
},
|
|
35698
35724
|
onExtensionDisconnect: (cb) => {
|
|
35699
35725
|
disconnectListeners.push(cb);
|
|
35700
|
-
}
|
|
35726
|
+
},
|
|
35727
|
+
onPendingPair: (cb) => {
|
|
35728
|
+
pendingPairListeners.push(cb);
|
|
35729
|
+
},
|
|
35730
|
+
pendingPairCode: () => ownPendingPairCode
|
|
35701
35731
|
};
|
|
35702
35732
|
}
|
|
35703
35733
|
|
|
@@ -35727,36 +35757,57 @@ async function startPeer(opts) {
|
|
|
35727
35757
|
const sessionNonce = fromB64(hello.sessionNonce);
|
|
35728
35758
|
ws.send(JSON.stringify(hello));
|
|
35729
35759
|
const innerListeners = [];
|
|
35760
|
+
const renegotiateListeners = [];
|
|
35761
|
+
const pendingPairListeners = [];
|
|
35730
35762
|
let session = null;
|
|
35763
|
+
let pendingPairCode = null;
|
|
35764
|
+
let resolveFirstReady;
|
|
35765
|
+
let rejectFirstReady;
|
|
35731
35766
|
const sessionPromise = new Promise((resolve, reject) => {
|
|
35732
|
-
|
|
35733
|
-
|
|
35734
|
-
|
|
35735
|
-
|
|
35736
|
-
|
|
35737
|
-
|
|
35738
|
-
|
|
35739
|
-
|
|
35740
|
-
|
|
35741
|
-
|
|
35742
|
-
|
|
35767
|
+
resolveFirstReady = resolve;
|
|
35768
|
+
rejectFirstReady = reject;
|
|
35769
|
+
});
|
|
35770
|
+
const onMessage = async (data) => {
|
|
35771
|
+
try {
|
|
35772
|
+
const raw = JSON.parse(data.toString());
|
|
35773
|
+
const frame = validateFrame(raw);
|
|
35774
|
+
if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
|
|
35775
|
+
const extPub = fromB64(frame.extensionSessionPub);
|
|
35776
|
+
const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
|
|
35777
|
+
const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
|
|
35778
|
+
const isRenegotiation = session !== null;
|
|
35779
|
+
session = new SessionState(sessionKey);
|
|
35780
|
+
pendingPairCode = null;
|
|
35781
|
+
if (isRenegotiation) {
|
|
35782
|
+
renegotiateListeners.forEach((cb) => cb());
|
|
35783
|
+
} else {
|
|
35784
|
+
resolveFirstReady(session);
|
|
35743
35785
|
}
|
|
35744
|
-
|
|
35745
|
-
|
|
35746
|
-
|
|
35747
|
-
|
|
35748
|
-
|
|
35786
|
+
return;
|
|
35787
|
+
}
|
|
35788
|
+
if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
|
|
35789
|
+
pendingPairCode = frame.pairCode;
|
|
35790
|
+
pendingPairListeners.forEach((cb) => cb(frame.pairCode));
|
|
35791
|
+
return;
|
|
35792
|
+
}
|
|
35793
|
+
if (frame.type === "frame" && frame.mcpId === opts.mcpId) {
|
|
35794
|
+
if (!session)
|
|
35795
|
+
return;
|
|
35796
|
+
if (!session.acceptInboundSeq(frame.seq))
|
|
35797
|
+
return;
|
|
35798
|
+
try {
|
|
35749
35799
|
const inner = await openEncryptedFrame(session.sessionKey, frame);
|
|
35750
35800
|
innerListeners.forEach((cb) => cb(inner));
|
|
35801
|
+
} catch {
|
|
35751
35802
|
}
|
|
35752
|
-
} catch (e) {
|
|
35753
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
35754
35803
|
}
|
|
35755
|
-
}
|
|
35756
|
-
|
|
35757
|
-
|
|
35758
|
-
|
|
35759
|
-
|
|
35804
|
+
} catch (e) {
|
|
35805
|
+
rejectFirstReady(e instanceof Error ? e : new Error(String(e)));
|
|
35806
|
+
}
|
|
35807
|
+
};
|
|
35808
|
+
ws.on("message", onMessage);
|
|
35809
|
+
ws.once("close", () => {
|
|
35810
|
+
rejectFirstReady(new Error("peer WS closed before ready"));
|
|
35760
35811
|
});
|
|
35761
35812
|
sessionPromise.catch(() => {
|
|
35762
35813
|
});
|
|
@@ -35764,13 +35815,21 @@ async function startPeer(opts) {
|
|
|
35764
35815
|
ws,
|
|
35765
35816
|
session: sessionPromise,
|
|
35766
35817
|
sendInner: async (inner) => {
|
|
35767
|
-
|
|
35818
|
+
await sessionPromise;
|
|
35819
|
+
const s = session;
|
|
35768
35820
|
const sealed = await sealInnerFrame(s.sessionKey, opts.mcpId, s.nextOutboundSeq(), inner);
|
|
35769
35821
|
ws.send(JSON.stringify(sealed));
|
|
35770
35822
|
},
|
|
35771
35823
|
onInner: (cb) => {
|
|
35772
35824
|
innerListeners.push(cb);
|
|
35773
35825
|
},
|
|
35826
|
+
onRenegotiate: (cb) => {
|
|
35827
|
+
renegotiateListeners.push(cb);
|
|
35828
|
+
},
|
|
35829
|
+
onPendingPair: (cb) => {
|
|
35830
|
+
pendingPairListeners.push(cb);
|
|
35831
|
+
},
|
|
35832
|
+
pendingPairCode: () => pendingPairCode,
|
|
35774
35833
|
close: () => ws.close()
|
|
35775
35834
|
};
|
|
35776
35835
|
return handle;
|
|
@@ -35895,7 +35954,11 @@ function assertUrlInDomains(field, url2, domains) {
|
|
|
35895
35954
|
}
|
|
35896
35955
|
var DEFAULT_JSON_OK_STATUSES = [200, 201, 202, 204];
|
|
35897
35956
|
var FetchproxyServer = class {
|
|
35898
|
-
/**
|
|
35957
|
+
/**
|
|
35958
|
+
* Bridge role. `null` until the first verb call (or an explicit
|
|
35959
|
+
* `connect()`) — `listen()` no longer triggers the role election
|
|
35960
|
+
* as of 0.5.3+. Reset to `null` on `close()`.
|
|
35961
|
+
*/
|
|
35899
35962
|
role = null;
|
|
35900
35963
|
opts;
|
|
35901
35964
|
hostHandle = null;
|
|
@@ -35917,6 +35980,12 @@ var FetchproxyServer = class {
|
|
|
35917
35980
|
pendingIdb = /* @__PURE__ */ new Map();
|
|
35918
35981
|
mcpId = null;
|
|
35919
35982
|
identity = null;
|
|
35983
|
+
// 0.5.3+: in-flight role-election / handle-start promise. Set the
|
|
35984
|
+
// first time a verb call runs `ensureConnected`, awaited by concurrent
|
|
35985
|
+
// callers, cleared once the connection is up. Single source of truth
|
|
35986
|
+
// for "we're connecting right now" so two parallel first-calls don't
|
|
35987
|
+
// race the port bind.
|
|
35988
|
+
connectingPromise = null;
|
|
35920
35989
|
constructor(opts) {
|
|
35921
35990
|
if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
|
|
35922
35991
|
throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
|
|
@@ -35968,23 +36037,87 @@ var FetchproxyServer = class {
|
|
|
35968
36037
|
};
|
|
35969
36038
|
}
|
|
35970
36039
|
/**
|
|
35971
|
-
*
|
|
35972
|
-
* from disk (creating it on first call)
|
|
35973
|
-
*
|
|
35974
|
-
*
|
|
35975
|
-
* `
|
|
35976
|
-
*
|
|
36040
|
+
* Prepare the bridge for use. Loads the long-term identity keypair
|
|
36041
|
+
* from disk (creating it on first call) and computes this instance's
|
|
36042
|
+
* `mcpId`. Does NOT bind the bridge port or dial any WebSocket — the
|
|
36043
|
+
* connection is established lazily on the first verb call (see
|
|
36044
|
+
* `ensureConnected` / `getOrConnect`).
|
|
36045
|
+
*
|
|
36046
|
+
* Pre-0.5.3 behavior: `listen()` also did role election and started
|
|
36047
|
+
* the host/peer immediately, which meant every configured-but-unused
|
|
36048
|
+
* MCP claimed bridge resources at MCP-client boot. Several MCPs
|
|
36049
|
+
* starting in parallel under Claude Desktop also produced noisy
|
|
36050
|
+
* `ERR_CONNECTION_REFUSED` errors in the extension if it raced ahead
|
|
36051
|
+
* of the first MCP's port bind. Deferring keeps boot quiet and
|
|
36052
|
+
* leaves the port unowned until something actually needs it.
|
|
36053
|
+
*
|
|
36054
|
+
* Calling `listen()` twice without an intervening `close()` is a
|
|
36055
|
+
* no-op (the second call's identity load is idempotent).
|
|
35977
36056
|
*/
|
|
35978
36057
|
async listen() {
|
|
35979
|
-
|
|
35980
|
-
|
|
36058
|
+
if (!this.identity) {
|
|
36059
|
+
this.identity = await loadOrCreateIdentity(this.opts.serverName, this.opts.identityDir);
|
|
36060
|
+
}
|
|
36061
|
+
if (!this.mcpId) {
|
|
36062
|
+
this.mcpId = generateMcpId(this.opts.serverName, this.opts.version);
|
|
36063
|
+
}
|
|
36064
|
+
}
|
|
36065
|
+
/**
|
|
36066
|
+
* Force an eager bridge connection (role-election + host/peer handle
|
|
36067
|
+
* start + listener wiring) without waiting for the first verb call.
|
|
36068
|
+
* Useful for callers that want to surface the role / connection
|
|
36069
|
+
* outcome at boot, or for tests whose harness dials a mock extension
|
|
36070
|
+
* immediately after server construction. Production MCPs that just
|
|
36071
|
+
* answer tool calls should NOT call this — the lazy connect via
|
|
36072
|
+
* `ensureConnected` will do the right thing on first use, keeping
|
|
36073
|
+
* boot cheap and avoiding port-bind contention for MCPs that never
|
|
36074
|
+
* actually get invoked.
|
|
36075
|
+
*
|
|
36076
|
+
* Idempotent: a second call after the first has resolved is a no-op
|
|
36077
|
+
* (the existing handle is reused). Throws if `listen()` was never
|
|
36078
|
+
* called.
|
|
36079
|
+
*/
|
|
36080
|
+
async connect() {
|
|
36081
|
+
await this.ensureConnected();
|
|
36082
|
+
}
|
|
36083
|
+
/**
|
|
36084
|
+
* Establish the bridge connection (role-election + host/peer handle
|
|
36085
|
+
* start + listener wiring) the first time a verb is invoked.
|
|
36086
|
+
* Idempotent after the connection is up; concurrent first-callers
|
|
36087
|
+
* share the same in-flight promise so only one election happens.
|
|
36088
|
+
*
|
|
36089
|
+
* Throws if `listen()` was never called — the contract is that the
|
|
36090
|
+
* MCP author still must wire `transport.start()` at boot to load
|
|
36091
|
+
* identity / set mcpId, even though the WS doesn't open until a
|
|
36092
|
+
* verb runs.
|
|
36093
|
+
*/
|
|
36094
|
+
async ensureConnected() {
|
|
36095
|
+
if (this.hostHandle || this.peerHandle)
|
|
36096
|
+
return;
|
|
36097
|
+
if (this.connectingPromise) {
|
|
36098
|
+
await this.connectingPromise;
|
|
36099
|
+
return;
|
|
36100
|
+
}
|
|
36101
|
+
if (!this.identity || !this.mcpId) {
|
|
36102
|
+
throw new Error("FetchproxyServer: ensureConnected called before listen() \u2014 call listen() at MCP boot to load identity");
|
|
36103
|
+
}
|
|
36104
|
+
this.connectingPromise = this.doConnect();
|
|
36105
|
+
try {
|
|
36106
|
+
await this.connectingPromise;
|
|
36107
|
+
} finally {
|
|
36108
|
+
this.connectingPromise = null;
|
|
36109
|
+
}
|
|
36110
|
+
}
|
|
36111
|
+
async doConnect() {
|
|
36112
|
+
const identity = this.identity;
|
|
36113
|
+
const mcpId = this.mcpId;
|
|
35981
36114
|
const el = await electRole({ host: this.opts.host, port: this.opts.port });
|
|
35982
36115
|
if (el.role === "host") {
|
|
35983
36116
|
this.role = "host";
|
|
35984
36117
|
this.hostHandle = await startHost({
|
|
35985
36118
|
httpServer: el.server,
|
|
35986
|
-
ownIdentity:
|
|
35987
|
-
ownMcpId:
|
|
36119
|
+
ownIdentity: identity,
|
|
36120
|
+
ownMcpId: mcpId,
|
|
35988
36121
|
ownServerName: this.opts.serverName,
|
|
35989
36122
|
ownVersion: this.opts.version,
|
|
35990
36123
|
ownDomains: this.opts.domains,
|
|
@@ -36000,13 +36133,16 @@ var FetchproxyServer = class {
|
|
|
36000
36133
|
});
|
|
36001
36134
|
this.hostHandle.onOwnInner((inner) => this.onInner(inner));
|
|
36002
36135
|
this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
|
|
36136
|
+
this.hostHandle.onPendingPair((code) => {
|
|
36137
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36138
|
+
});
|
|
36003
36139
|
} else {
|
|
36004
36140
|
this.role = "peer";
|
|
36005
36141
|
this.peerHandle = await startPeer({
|
|
36006
36142
|
host: this.opts.host,
|
|
36007
36143
|
port: this.opts.port,
|
|
36008
|
-
identity
|
|
36009
|
-
mcpId
|
|
36144
|
+
identity,
|
|
36145
|
+
mcpId,
|
|
36010
36146
|
serverName: this.opts.serverName,
|
|
36011
36147
|
version: this.opts.version,
|
|
36012
36148
|
domains: this.opts.domains,
|
|
@@ -36020,8 +36156,19 @@ var FetchproxyServer = class {
|
|
|
36020
36156
|
sessionStoragePointers: this.opts.sessionStoragePointers
|
|
36021
36157
|
});
|
|
36022
36158
|
this.peerHandle.onInner((inner) => this.onInner(inner));
|
|
36159
|
+
this.peerHandle.onRenegotiate(() => this.rejectAllPending());
|
|
36160
|
+
this.peerHandle.onPendingPair((code) => {
|
|
36161
|
+
this.rejectAllPending(this.pairingErrorMessage(code));
|
|
36162
|
+
});
|
|
36163
|
+
if (this.opts.onPairCode) {
|
|
36164
|
+
const cb = this.opts.onPairCode;
|
|
36165
|
+
this.peerHandle.onPendingPair((code) => cb(code));
|
|
36166
|
+
}
|
|
36023
36167
|
}
|
|
36024
36168
|
}
|
|
36169
|
+
pairingErrorMessage(code) {
|
|
36170
|
+
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}`;
|
|
36171
|
+
}
|
|
36025
36172
|
/**
|
|
36026
36173
|
* Raw single-shot fetch through the bridge. Most callers should prefer
|
|
36027
36174
|
* the verb shortcuts (`get` / `post` / `getJson` / `postJson` / `getHtml`)
|
|
@@ -36037,8 +36184,11 @@ var FetchproxyServer = class {
|
|
|
36037
36184
|
* offline, etc.).
|
|
36038
36185
|
*/
|
|
36039
36186
|
async fetch(init) {
|
|
36040
|
-
|
|
36041
|
-
|
|
36187
|
+
await this.ensureConnected();
|
|
36188
|
+
const pendingCode = this.currentPendingPairCode();
|
|
36189
|
+
if (pendingCode !== null) {
|
|
36190
|
+
const error51 = this.pairingErrorMessage(pendingCode);
|
|
36191
|
+
return { ok: false, error: error51, kind: classifyFetchError(error51) };
|
|
36042
36192
|
}
|
|
36043
36193
|
const id = this.nextRequestId++;
|
|
36044
36194
|
const inner = { type: "request", id, op: "fetch", init };
|
|
@@ -36179,9 +36329,8 @@ var FetchproxyServer = class {
|
|
|
36179
36329
|
if (!this.opts.capabilities.includes("read_cookies")) {
|
|
36180
36330
|
throw new Error('FetchproxyServer.readCookies(): MCP did not declare "read_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
|
|
36181
36331
|
}
|
|
36182
|
-
|
|
36183
|
-
|
|
36184
|
-
}
|
|
36332
|
+
await this.ensureConnected();
|
|
36333
|
+
this.throwIfPendingPair();
|
|
36185
36334
|
if (opts.subdomain !== void 0)
|
|
36186
36335
|
assertSubdomainLabel(opts.subdomain);
|
|
36187
36336
|
const baseDomain = this.resolveBaseDomain(opts.domain);
|
|
@@ -36238,9 +36387,8 @@ var FetchproxyServer = class {
|
|
|
36238
36387
|
if (!this.opts.capabilities.includes(op)) {
|
|
36239
36388
|
throw new Error(`FetchproxyServer.${op === "read_local_storage" ? "readLocalStorage" : "readSessionStorage"}(): MCP did not declare ${JSON.stringify(op)} in capabilities`);
|
|
36240
36389
|
}
|
|
36241
|
-
|
|
36242
|
-
|
|
36243
|
-
}
|
|
36390
|
+
await this.ensureConnected();
|
|
36391
|
+
this.throwIfPendingPair();
|
|
36244
36392
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36245
36393
|
throw new Error(`FetchproxyServer.${op}: opts.keys must be a non-empty array`);
|
|
36246
36394
|
}
|
|
@@ -36295,9 +36443,8 @@ var FetchproxyServer = class {
|
|
|
36295
36443
|
if (!this.opts.capabilities.includes("capture_request_header")) {
|
|
36296
36444
|
throw new Error('FetchproxyServer.captureRequestHeader(): MCP did not declare "capture_request_header" in capabilities');
|
|
36297
36445
|
}
|
|
36298
|
-
|
|
36299
|
-
|
|
36300
|
-
}
|
|
36446
|
+
await this.ensureConnected();
|
|
36447
|
+
this.throwIfPendingPair();
|
|
36301
36448
|
const declared = this.opts.captureHeaders.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
|
|
36302
36449
|
if (!declared) {
|
|
36303
36450
|
throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
|
|
@@ -36338,9 +36485,8 @@ var FetchproxyServer = class {
|
|
|
36338
36485
|
if (!this.opts.capabilities.includes("read_indexed_db")) {
|
|
36339
36486
|
throw new Error('FetchproxyServer.readIndexedDb(): MCP did not declare "read_indexed_db" in capabilities');
|
|
36340
36487
|
}
|
|
36341
|
-
|
|
36342
|
-
|
|
36343
|
-
}
|
|
36488
|
+
await this.ensureConnected();
|
|
36489
|
+
this.throwIfPendingPair();
|
|
36344
36490
|
if (!Array.isArray(opts.keys) || opts.keys.length === 0) {
|
|
36345
36491
|
throw new Error("FetchproxyServer.readIndexedDb: opts.keys must be a non-empty array");
|
|
36346
36492
|
}
|
|
@@ -36489,8 +36635,8 @@ var FetchproxyServer = class {
|
|
|
36489
36635
|
}
|
|
36490
36636
|
}
|
|
36491
36637
|
}
|
|
36492
|
-
rejectAllPending() {
|
|
36493
|
-
const err = new FetchproxyProtocolError(
|
|
36638
|
+
rejectAllPending(reason = "extension disconnected") {
|
|
36639
|
+
const err = new FetchproxyProtocolError(reason);
|
|
36494
36640
|
for (const cb of this.pending.values()) {
|
|
36495
36641
|
cb({ ok: false, error: err.message, kind: classifyFetchError(err.message) });
|
|
36496
36642
|
}
|
|
@@ -36509,6 +36655,32 @@ var FetchproxyServer = class {
|
|
|
36509
36655
|
reject(err);
|
|
36510
36656
|
this.pendingIdb.clear();
|
|
36511
36657
|
}
|
|
36658
|
+
/**
|
|
36659
|
+
* 0.5.2+: read the current pair-pending pair code from whichever handle
|
|
36660
|
+
* is active, returning null when none is pending. Public verbs call this
|
|
36661
|
+
* at the top so that a tool invoked while the bridge is waiting on user
|
|
36662
|
+
* approval fails fast with the actionable error rather than hanging on a
|
|
36663
|
+
* sealed frame the extension will never process.
|
|
36664
|
+
*/
|
|
36665
|
+
currentPendingPairCode() {
|
|
36666
|
+
if (this.hostHandle)
|
|
36667
|
+
return this.hostHandle.pendingPairCode();
|
|
36668
|
+
if (this.peerHandle)
|
|
36669
|
+
return this.peerHandle.pendingPairCode();
|
|
36670
|
+
return null;
|
|
36671
|
+
}
|
|
36672
|
+
/**
|
|
36673
|
+
* 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
|
|
36674
|
+
* message if the bridge is waiting on user approval. Used by the verb
|
|
36675
|
+
* methods (readCookies, readLocalStorage, etc.) that surface errors via
|
|
36676
|
+
* thrown exceptions rather than `ok:false` discriminated unions.
|
|
36677
|
+
*/
|
|
36678
|
+
throwIfPendingPair() {
|
|
36679
|
+
const code = this.currentPendingPairCode();
|
|
36680
|
+
if (code !== null) {
|
|
36681
|
+
throw new FetchproxyProtocolError(this.pairingErrorMessage(code));
|
|
36682
|
+
}
|
|
36683
|
+
}
|
|
36512
36684
|
/**
|
|
36513
36685
|
* Shut down the bridge. Host: terminates the WebSocket server and any
|
|
36514
36686
|
* still-attached extension/peer clients. Peer: closes the upstream
|
|
@@ -36517,6 +36689,9 @@ var FetchproxyServer = class {
|
|
|
36517
36689
|
*/
|
|
36518
36690
|
async close() {
|
|
36519
36691
|
this.rejectAllPending();
|
|
36692
|
+
if (this.connectingPromise) {
|
|
36693
|
+
await this.connectingPromise.catch(() => void 0);
|
|
36694
|
+
}
|
|
36520
36695
|
if (this.hostHandle)
|
|
36521
36696
|
await this.hostHandle.close();
|
|
36522
36697
|
if (this.peerHandle)
|
|
@@ -36524,6 +36699,7 @@ var FetchproxyServer = class {
|
|
|
36524
36699
|
this.hostHandle = null;
|
|
36525
36700
|
this.peerHandle = null;
|
|
36526
36701
|
this.role = null;
|
|
36702
|
+
this.connectingPromise = null;
|
|
36527
36703
|
}
|
|
36528
36704
|
};
|
|
36529
36705
|
|
|
@@ -36772,7 +36948,7 @@ function loadAccount(env = process.env) {
|
|
|
36772
36948
|
// package.json
|
|
36773
36949
|
var package_default = {
|
|
36774
36950
|
name: "canvas-parent-mcp",
|
|
36775
|
-
version: "1.1.
|
|
36951
|
+
version: "1.1.3",
|
|
36776
36952
|
mcpName: "io.github.chrischall/canvas-parent-mcp",
|
|
36777
36953
|
description: "Canvas LMS (Instructure) MCP server \u2014 student/observer read access with optional file download",
|
|
36778
36954
|
author: "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -36816,17 +36992,17 @@ var package_default = {
|
|
|
36816
36992
|
"test:watch": "vitest"
|
|
36817
36993
|
},
|
|
36818
36994
|
dependencies: {
|
|
36819
|
-
"@fetchproxy/bootstrap": "^0.
|
|
36995
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
36820
36996
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
36821
|
-
dotenv: "^17.4.
|
|
36822
|
-
zod: "^4.3
|
|
36997
|
+
dotenv: "^17.4.2",
|
|
36998
|
+
zod: "^4.4.3"
|
|
36823
36999
|
},
|
|
36824
37000
|
devDependencies: {
|
|
36825
|
-
"@types/node": "^25.
|
|
36826
|
-
"@vitest/coverage-v8": "^4.1.
|
|
37001
|
+
"@types/node": "^25.9.1",
|
|
37002
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
36827
37003
|
esbuild: "^0.28.0",
|
|
36828
|
-
typescript: "^6.0.
|
|
36829
|
-
vitest: "^4.1.
|
|
37004
|
+
typescript: "^6.0.3",
|
|
37005
|
+
vitest: "^4.1.7"
|
|
36830
37006
|
}
|
|
36831
37007
|
};
|
|
36832
37008
|
|
|
@@ -37618,7 +37794,7 @@ try {
|
|
|
37618
37794
|
} catch (e) {
|
|
37619
37795
|
configError = e;
|
|
37620
37796
|
}
|
|
37621
|
-
var server = new McpServer({ name: "canvas", version: "1.1.
|
|
37797
|
+
var server = new McpServer({ name: "canvas", version: "1.1.3" });
|
|
37622
37798
|
if (resolved) {
|
|
37623
37799
|
const client = new CanvasClient(resolved.account, {
|
|
37624
37800
|
preloaded: resolved.preloaded
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ try {
|
|
|
44
44
|
catch (e) {
|
|
45
45
|
configError = e;
|
|
46
46
|
}
|
|
47
|
-
const server = new McpServer({ name: 'canvas', version: '1.1.
|
|
47
|
+
const server = new McpServer({ name: 'canvas', version: '1.1.3' }); // x-release-please-version
|
|
48
48
|
if (resolved) {
|
|
49
49
|
const client = new CanvasClient(resolved.account, {
|
|
50
50
|
preloaded: resolved.preloaded,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canvas-parent-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"mcpName": "io.github.chrischall/canvas-parent-mcp",
|
|
5
5
|
"description": "Canvas LMS (Instructure) MCP server — student/observer read access with optional file download",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -44,16 +44,16 @@
|
|
|
44
44
|
"test:watch": "vitest"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@fetchproxy/bootstrap": "^0.
|
|
47
|
+
"@fetchproxy/bootstrap": "^0.6.0",
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
-
"dotenv": "^17.4.
|
|
50
|
-
"zod": "^4.3
|
|
49
|
+
"dotenv": "^17.4.2",
|
|
50
|
+
"zod": "^4.4.3"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@types/node": "^25.
|
|
54
|
-
"@vitest/coverage-v8": "^4.1.
|
|
53
|
+
"@types/node": "^25.9.1",
|
|
54
|
+
"@vitest/coverage-v8": "^4.1.7",
|
|
55
55
|
"esbuild": "^0.28.0",
|
|
56
|
-
"typescript": "^6.0.
|
|
57
|
-
"vitest": "^4.1.
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vitest": "^4.1.7"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/chrischall/canvas-parent-mcp",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.1.
|
|
9
|
+
"version": "1.1.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "canvas-parent-mcp",
|
|
14
|
-
"version": "1.1.
|
|
14
|
+
"version": "1.1.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|