@usecontextlayer/ctxs 0.5.14 → 0.5.16
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/cli.mjs +3092 -264
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
3
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="3bf0713e-bd74-5249-b278-e30de2c2d243")}catch(e){}}();
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import * as Sentry from "@sentry/node";
|
|
6
6
|
import * as fs$2 from "node:fs/promises";
|
|
@@ -499,6 +499,59 @@ function formatError$2(error, mapper = (issue) => issue.message) {
|
|
|
499
499
|
processError(error);
|
|
500
500
|
return fieldErrors;
|
|
501
501
|
}
|
|
502
|
+
/** Format a ZodError as a human-readable string in the following form.
|
|
503
|
+
*
|
|
504
|
+
* From
|
|
505
|
+
*
|
|
506
|
+
* ```ts
|
|
507
|
+
* ZodError {
|
|
508
|
+
* issues: [
|
|
509
|
+
* {
|
|
510
|
+
* expected: 'string',
|
|
511
|
+
* code: 'invalid_type',
|
|
512
|
+
* path: [ 'username' ],
|
|
513
|
+
* message: 'Invalid input: expected string'
|
|
514
|
+
* },
|
|
515
|
+
* {
|
|
516
|
+
* expected: 'number',
|
|
517
|
+
* code: 'invalid_type',
|
|
518
|
+
* path: [ 'favoriteNumbers', 1 ],
|
|
519
|
+
* message: 'Invalid input: expected number'
|
|
520
|
+
* }
|
|
521
|
+
* ];
|
|
522
|
+
* }
|
|
523
|
+
* ```
|
|
524
|
+
*
|
|
525
|
+
* to
|
|
526
|
+
*
|
|
527
|
+
* ```
|
|
528
|
+
* username
|
|
529
|
+
* ✖ Expected number, received string at "username
|
|
530
|
+
* favoriteNumbers[0]
|
|
531
|
+
* ✖ Invalid input: expected number
|
|
532
|
+
* ```
|
|
533
|
+
*/
|
|
534
|
+
function toDotPath(_path) {
|
|
535
|
+
const segs = [];
|
|
536
|
+
const path = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
537
|
+
for (const seg of path) if (typeof seg === "number") segs.push(`[${seg}]`);
|
|
538
|
+
else if (typeof seg === "symbol") segs.push(`[${JSON.stringify(String(seg))}]`);
|
|
539
|
+
else if (/[^\w$]/.test(seg)) segs.push(`[${JSON.stringify(seg)}]`);
|
|
540
|
+
else {
|
|
541
|
+
if (segs.length) segs.push(".");
|
|
542
|
+
segs.push(seg);
|
|
543
|
+
}
|
|
544
|
+
return segs.join("");
|
|
545
|
+
}
|
|
546
|
+
function prettifyError(error) {
|
|
547
|
+
const lines = [];
|
|
548
|
+
const issues = [...error.issues].sort((a, b) => (a.path ?? []).length - (b.path ?? []).length);
|
|
549
|
+
for (const issue of issues) {
|
|
550
|
+
lines.push(`✖ ${issue.message}`);
|
|
551
|
+
if (issue.path?.length) lines.push(` → at ${toDotPath(issue.path)}`);
|
|
552
|
+
}
|
|
553
|
+
return lines.join("\n");
|
|
554
|
+
}
|
|
502
555
|
|
|
503
556
|
//#endregion
|
|
504
557
|
//#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js
|
|
@@ -4722,7 +4775,7 @@ function normalizedKey(event) {
|
|
|
4722
4775
|
|
|
4723
4776
|
//#endregion
|
|
4724
4777
|
//#region package.json
|
|
4725
|
-
var version$1 = "0.5.
|
|
4778
|
+
var version$1 = "0.5.16";
|
|
4726
4779
|
|
|
4727
4780
|
//#endregion
|
|
4728
4781
|
//#region sentry.ts
|
|
@@ -5088,8 +5141,10 @@ async function cloneRepo(repo) {
|
|
|
5088
5141
|
], { auth: repo.remote });
|
|
5089
5142
|
}
|
|
5090
5143
|
/**
|
|
5091
|
-
* Snapshot the working dir to pggit: `add -A` -> commit -> plain `push`.
|
|
5092
|
-
* nothing changed
|
|
5144
|
+
* Snapshot the working dir to pggit: `add -A` -> optional commit -> plain `push`. When
|
|
5145
|
+
* nothing changed, no commit is created, but an existing HEAD is still pushed. That
|
|
5146
|
+
* retries a prior commit whose push failed without moving the retry outside the caller's
|
|
5147
|
+
* serialization boundary.
|
|
5093
5148
|
* Excludes come from a `.gitignore` the caller commits and owns (ClaudeRepo's
|
|
5094
5149
|
* CLAUDE_HOME_GITIGNORE is the claude-home one), not from this function.
|
|
5095
5150
|
* Self-initializes a fresh dir, so it works for both the
|
|
@@ -5110,8 +5165,9 @@ async function pushSnapshot(repo, opts) {
|
|
|
5110
5165
|
"diff",
|
|
5111
5166
|
"--cached",
|
|
5112
5167
|
"--name-only"
|
|
5113
|
-
], { cwd: repo.hostDir })).stdout.trim() === "")
|
|
5114
|
-
|
|
5168
|
+
], { cwd: repo.hostDir })).stdout.trim() === "") {
|
|
5169
|
+
if (await localHead(repo) === null) return;
|
|
5170
|
+
} else await runGit([
|
|
5115
5171
|
"commit",
|
|
5116
5172
|
"-m",
|
|
5117
5173
|
opts.message
|
|
@@ -5122,6 +5178,9 @@ async function pushSnapshot(repo, opts) {
|
|
|
5122
5178
|
name: CAPTURE_AUTHOR_NAME
|
|
5123
5179
|
}
|
|
5124
5180
|
});
|
|
5181
|
+
await pushHead(repo);
|
|
5182
|
+
}
|
|
5183
|
+
async function pushHead(repo) {
|
|
5125
5184
|
await runGit([
|
|
5126
5185
|
"push",
|
|
5127
5186
|
"origin",
|
|
@@ -5275,13 +5334,29 @@ async function createIndex(claudeRootDir) {
|
|
|
5275
5334
|
async function sessionExists(claudeRootDir, sessionId) {
|
|
5276
5335
|
return (await findSessionFiles(claudeRootDir)).some((file) => path.basename(file, ".jsonl") === sessionId);
|
|
5277
5336
|
}
|
|
5337
|
+
/**
|
|
5338
|
+
* The absolute path of `sessionId`'s transcript, or null when this home holds none. Same
|
|
5339
|
+
* glob as `sessionExists`, but removal needs the path, not the yes/no.
|
|
5340
|
+
*
|
|
5341
|
+
* Deliberately STRICTER than `sessionExists`, and only here: two matching transcripts in
|
|
5342
|
+
* one home make the path ambiguous, and a removal that silently picked one would leave the
|
|
5343
|
+
* other behind and still report success. Boot stays lenient because its answer picks a
|
|
5344
|
+
* FLAG, never a file to mutate — ambiguity costs it nothing, and throwing there would fail
|
|
5345
|
+
* the whole chat over a condition that path can survive.
|
|
5346
|
+
*/
|
|
5347
|
+
async function findSessionFile(claudeRootDir, sessionId) {
|
|
5348
|
+
const matches = (await findSessionFiles(claudeRootDir)).filter((file) => path.basename(file, ".jsonl") === sessionId);
|
|
5349
|
+
if (matches.length > 1) throw new Error(`claude home ${claudeRootDir}: session ${sessionId} matched ${matches.length} transcripts (expected at most one)`);
|
|
5350
|
+
return matches[0] ?? null;
|
|
5351
|
+
}
|
|
5278
5352
|
async function findSessionFiles(claudeRootDir) {
|
|
5279
5353
|
const projects = path.join(claudeRootDir, "projects");
|
|
5280
5354
|
let projectDirs;
|
|
5281
5355
|
try {
|
|
5282
5356
|
projectDirs = await readdir(projects);
|
|
5283
|
-
} catch {
|
|
5284
|
-
return [];
|
|
5357
|
+
} catch (err) {
|
|
5358
|
+
if (isNodeError(err) && err.code === "ENOENT") return [];
|
|
5359
|
+
throw err;
|
|
5285
5360
|
}
|
|
5286
5361
|
const limit = pLimit(READ_CONCURRENCY);
|
|
5287
5362
|
return (await Promise.all(projectDirs.map((dir) => limit(async () => {
|
|
@@ -5289,8 +5364,9 @@ async function findSessionFiles(claudeRootDir) {
|
|
|
5289
5364
|
let entries;
|
|
5290
5365
|
try {
|
|
5291
5366
|
entries = await readdir(full);
|
|
5292
|
-
} catch {
|
|
5293
|
-
return [];
|
|
5367
|
+
} catch (err) {
|
|
5368
|
+
if (isNodeError(err) && (err.code === "ENOENT" || err.code === "ENOTDIR")) return [];
|
|
5369
|
+
throw err;
|
|
5294
5370
|
}
|
|
5295
5371
|
return entries.filter((e) => e.endsWith(".jsonl")).map((e) => path.join(full, e));
|
|
5296
5372
|
})))).flat();
|
|
@@ -5408,13 +5484,46 @@ const INDEX_FILENAME = ".ctx-sessions.json";
|
|
|
5408
5484
|
* policy in home-watch-plan.ts) plus the caller's teardown pass — one-sandbox-per-session,
|
|
5409
5485
|
* and the session index (rebuilt whole each capture).
|
|
5410
5486
|
*
|
|
5411
|
-
*
|
|
5412
|
-
* filesystem is live truth, and ClaudeRepo periodically snapshots it
|
|
5413
|
-
* consistency). A snapshot may capture MID-WRITE state — a pushed transcript can
|
|
5414
|
-
* a torn half-line — so raw-home readers must tolerate partial tails (the index
|
|
5415
|
-
* does).
|
|
5416
|
-
*
|
|
5417
|
-
*
|
|
5487
|
+
* Claude's live transcript writes do NOT go through ClaudeRepo — claude writes the mounted
|
|
5488
|
+
* dir directly, the filesystem is live truth, and ClaudeRepo periodically snapshots it
|
|
5489
|
+
* (eventual consistency). A snapshot may capture MID-WRITE state — a pushed transcript can
|
|
5490
|
+
* end in a torn half-line — so raw-home readers must tolerate partial tails (the index
|
|
5491
|
+
* reader does).
|
|
5492
|
+
*
|
|
5493
|
+
* ## SINGLE-WRITER IS A DEPLOYMENT CONSTRAINT, NOT AN INTERNAL DETAIL
|
|
5494
|
+
*
|
|
5495
|
+
* **One claude home has exactly ONE owning process at a time. ONE container, ONE bridge
|
|
5496
|
+
* process for that process lifetime. Concurrent processes serving the same repoId are a
|
|
5497
|
+
* DATA-LOSS BUG, not a scaling knob.** Everything below rests on it, and nothing in this
|
|
5498
|
+
* class can detect its violation.
|
|
5499
|
+
*
|
|
5500
|
+
* This class is WRITE-ONLY against the remote after its single clone: `restore()` clones
|
|
5501
|
+
* once and memoizes that forever, and `capture()` only ever pushes. There is NO fetch, no
|
|
5502
|
+
* pull, no merge, no fast-forward — deliberately, and unlike the multi-writer workspace
|
|
5503
|
+
* repo, whose `ensureWorkspaceCheckout` runs the full landlord ladder (clone-if-absent,
|
|
5504
|
+
* else compare heads and fast-forward) precisely because it has more than one writer. The
|
|
5505
|
+
* asymmetry is the design, not an omission.
|
|
5506
|
+
*
|
|
5507
|
+
* Two consequences a reader must hold:
|
|
5508
|
+
*
|
|
5509
|
+
* 1. **The host dir is authoritative for reads only while no other writer advances the
|
|
5510
|
+
* remote after this process clones it.** Anything that asks "does this home contain
|
|
5511
|
+
* X?" — `hasSession`, `removeTranscript` — answers from this process's working copy.
|
|
5512
|
+
* A second writer can make that checkout stale and every such read silently wrong.
|
|
5513
|
+
* 2. **A second writer breaks periodic persistence outside the request path.** Pushes are
|
|
5514
|
+
* non-force, so the loser's `capture()` rejects non-fast-forward. The convergence
|
|
5515
|
+
* watcher and connection teardown catch and log that rejection, so chats stop saving
|
|
5516
|
+
* without failing a request. Removal deliberately propagates it to the request.
|
|
5517
|
+
*
|
|
5518
|
+
* What the premise forbids, concretely: a second bridge replica; two containers sharing a
|
|
5519
|
+
* host-dir volume; any out-of-band writer pushing to a `claude/slate/…` repo. The
|
|
5520
|
+
* deployment-level ownership contract lives in `ARCHITECTURE.md`.
|
|
5521
|
+
*
|
|
5522
|
+
* **Before adding a replica, a persistent host-dir volume, or any second writer, this
|
|
5523
|
+
* class must first learn to read** — clone-or-fast-forward on entry, and a real answer for
|
|
5524
|
+
* "the remote moved under me". Adding only the clone-if-absent guard is the specific trap:
|
|
5525
|
+
* it suppresses `git clone`'s LOUD non-empty-destination failure and permits a silent read
|
|
5526
|
+
* of a stale working copy. See `restore()`, where that guard would go.
|
|
5418
5527
|
*/
|
|
5419
5528
|
var ClaudeRepo = class ClaudeRepo {
|
|
5420
5529
|
static registry = /* @__PURE__ */ new Map();
|
|
@@ -5437,6 +5546,7 @@ var ClaudeRepo = class ClaudeRepo {
|
|
|
5437
5546
|
repo;
|
|
5438
5547
|
config;
|
|
5439
5548
|
activeSessions = /* @__PURE__ */ new Set();
|
|
5549
|
+
pendingRemovalSessionIds = /* @__PURE__ */ new Set();
|
|
5440
5550
|
captureChain = Promise.resolve();
|
|
5441
5551
|
restorePromise = null;
|
|
5442
5552
|
watchState = INITIAL_WATCH_STATE;
|
|
@@ -5497,6 +5607,21 @@ var ClaudeRepo = class ClaudeRepo {
|
|
|
5497
5607
|
* Every success — fresh clone or memoized warm reconnect — arms the watcher when a
|
|
5498
5608
|
* session is live: the watcher stops with the last release, so a reconnect to the
|
|
5499
5609
|
* kept-warm singleton must re-arm here or the whole session runs unconverged.
|
|
5610
|
+
*
|
|
5611
|
+
* ⚠️ THE UNCONDITIONAL `cloneRepo` IS LOAD-BEARING. DO NOT ADD AN `isCloned` GUARD.
|
|
5612
|
+
* `git clone` refuses a non-empty destination, so a process that starts over a host dir
|
|
5613
|
+
* some earlier process left behind fails HERE, loudly, and blocks the connection. That
|
|
5614
|
+
* throw is the only thing standing between us and a silent stale read: the memo lives in
|
|
5615
|
+
* memory while the dir lives on disk, so a warm dir + a cold registry is exactly the
|
|
5616
|
+
* state where the local copy may no longer match the remote, and every host-dir read
|
|
5617
|
+
* (`hasSession`, `removeTranscript`) would quietly answer from a stale tree. Guarding
|
|
5618
|
+
* the clone trades that loud failure for a wrong answer nobody sees.
|
|
5619
|
+
*
|
|
5620
|
+
* Reusing a warm dir safely needs clone-OR-fast-forward — `ensureWorkspaceCheckout`'s
|
|
5621
|
+
* ladder — which makes this class a READER of its remote and retires the single-writer
|
|
5622
|
+
* premise the class doc sets out. That is a real change with its own decision, not a guard.
|
|
5623
|
+
* (The guard is pinned by the prior-clone case in
|
|
5624
|
+
* packages/slate-bridge/tests/chat-session-delete-cold-home.node.integration.test.ts.)
|
|
5500
5625
|
*/
|
|
5501
5626
|
async restore() {
|
|
5502
5627
|
if (!this.config.restore) return;
|
|
@@ -5518,37 +5643,89 @@ var ClaudeRepo = class ClaudeRepo {
|
|
|
5518
5643
|
* The home snapshot (fired by the convergence watcher and the caller's teardown
|
|
5519
5644
|
* pass): serialized by the mutex — rebuild the index from the live
|
|
5520
5645
|
* home (when maintaining it), then push the whole dir (non-force; a home is
|
|
5521
|
-
* single-writer so the push always fast-forwards).
|
|
5522
|
-
*
|
|
5523
|
-
*
|
|
5524
|
-
*
|
|
5525
|
-
*
|
|
5526
|
-
*
|
|
5527
|
-
*
|
|
5528
|
-
*
|
|
5646
|
+
* single-writer so the push always fast-forwards).
|
|
5647
|
+
*
|
|
5648
|
+
* **This PROPAGATES.** Whether a failed snapshot matters is the caller's question, and the
|
|
5649
|
+
* two answers genuinely differ. The periodic pushes — the convergence watcher and the
|
|
5650
|
+
* teardown `(final)` pass — swallow it explicitly at their own call sites, because the
|
|
5651
|
+
* bytes are already on disk and the next snapshot pushes the cumulative state. A REMOVAL
|
|
5652
|
+
* cannot borrow that argument: its only pending change IS the push that just failed, so
|
|
5653
|
+
* there is no later snapshot guaranteed to carry it, and a caller is waiting on the answer.
|
|
5654
|
+
* `removeTranscript` therefore lets the failure through.
|
|
5655
|
+
*
|
|
5656
|
+
* The push auth is the connection's upgrade-time user token. If it expires, snapshots
|
|
5657
|
+
* fail until another connection refreshes this repo's token; unpushed turns are then
|
|
5658
|
+
* vulnerable to loss if the host dir disappears first. Token lifetime is owned by web,
|
|
5659
|
+
* not this repository.
|
|
5660
|
+
*
|
|
5661
|
+
* The mutex chain deliberately does NOT inherit this run's rejection. A chain parked in a
|
|
5662
|
+
* rejected state would fail every snapshot that later queues behind it, turning one bad
|
|
5663
|
+
* push into a permanently dead home — and would make `settle()` reject, which its callers
|
|
5664
|
+
* (teardown paths) rely on never happening. The chain carries ordering only; the failure
|
|
5665
|
+
* goes to whoever called.
|
|
5529
5666
|
*/
|
|
5530
5667
|
async capture(opts) {
|
|
5531
5668
|
const run = this.captureChain.then(async () => {
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
await pushSnapshot(this.repo, { message: opts.message });
|
|
5536
|
-
} catch (err) {
|
|
5537
|
-
console.error(`[ClaudeRepo] capture failed (${this.repo.remote.repoId}):`, err);
|
|
5538
|
-
}
|
|
5669
|
+
await this.ensureGitignore();
|
|
5670
|
+
if (this.config.maintainIndex) await this.writeSessionIndex();
|
|
5671
|
+
await pushSnapshot(this.repo, { message: opts.message });
|
|
5539
5672
|
});
|
|
5540
|
-
this.captureChain = run;
|
|
5673
|
+
this.captureChain = run.then(() => void 0, () => void 0);
|
|
5541
5674
|
return run;
|
|
5542
5675
|
}
|
|
5543
5676
|
/**
|
|
5544
5677
|
* Await the capture chain's quiescence — the shutdown flush. Captures are fired
|
|
5545
5678
|
* fire-and-forget by the watcher and the caller's teardown pass; the scope that
|
|
5546
5679
|
* fired them awaits this before tearing anything down, so no snapshot outlives
|
|
5547
|
-
* its host dir. Never rejects
|
|
5680
|
+
* its host dir. Never rejects — it awaits the ordering chain, which `capture` keeps
|
|
5681
|
+
* rejection-free on purpose, not the snapshot promises themselves.
|
|
5548
5682
|
*/
|
|
5549
5683
|
async settle() {
|
|
5550
5684
|
await this.captureChain;
|
|
5551
5685
|
}
|
|
5686
|
+
/**
|
|
5687
|
+
* Remove one session's transcript from the home and snapshot the removal — the GIT half
|
|
5688
|
+
* of deleting a chat, and the only half this class owns. Returns false when the home
|
|
5689
|
+
* holds no such transcript: the chat id is minted client-side and navigated to before
|
|
5690
|
+
* the first turn persists, so deleting a never-used chat is a legitimate no-op.
|
|
5691
|
+
*
|
|
5692
|
+
* The index needs no separate edit — `capture` rebuilds it whole from whatever
|
|
5693
|
+
* transcripts remain, which is why removal costs no index-maintenance code.
|
|
5694
|
+
*
|
|
5695
|
+
* THROWS when removing or snapshotting fails, and that is the point: a failure does not
|
|
5696
|
+
* prove the remote deletion. Once `rm` succeeds, the local state depends on which git step
|
|
5697
|
+
* failed — the deletion may be unstaged, staged, committed, or already accepted remotely.
|
|
5698
|
+
* A retry whose file is already absent re-runs the serialized snapshot, which always
|
|
5699
|
+
* pushes an existing HEAD even when it finds no new diff. Failures before or after commit
|
|
5700
|
+
* therefore cannot turn into a false 204.
|
|
5701
|
+
*
|
|
5702
|
+
* PRECONDITION, the caller's alone: no writer for this session's transcript may remain.
|
|
5703
|
+
* A live guest re-creates the file it is appending to, and its connection's teardown
|
|
5704
|
+
* capture pushes whatever it left behind — silently undoing the removal. This class
|
|
5705
|
+
* holds session IDENTITY, never a handle to the compute, so it cannot check that itself;
|
|
5706
|
+
* whoever owns the sandbox lifecycle must stop and join every connection serving this
|
|
5707
|
+
* session, then `settle()` the shared repo's already-queued captures first.
|
|
5708
|
+
*
|
|
5709
|
+
* Do NOT source existence from the remote instead (the `repo_file` query the read plane
|
|
5710
|
+
* uses). It would make the ANSWER survive a stale working copy but not the OPERATION —
|
|
5711
|
+
* a removal still cannot delete what its working copy does not contain — so it buys a
|
|
5712
|
+
* loud wrong answer in place of a quiet one, and puts two sources of truth inside one
|
|
5713
|
+
* operation.
|
|
5714
|
+
*/
|
|
5715
|
+
async removeTranscript(sessionId) {
|
|
5716
|
+
const file = await findSessionFile(this.repo.hostDir, sessionId);
|
|
5717
|
+
if (file === null) {
|
|
5718
|
+
if (!this.pendingRemovalSessionIds.has(sessionId)) return false;
|
|
5719
|
+
await this.capture({ message: `chat ${this.repo.remote.repoId} (delete ${sessionId})` });
|
|
5720
|
+
this.pendingRemovalSessionIds.delete(sessionId);
|
|
5721
|
+
return false;
|
|
5722
|
+
}
|
|
5723
|
+
this.pendingRemovalSessionIds.add(sessionId);
|
|
5724
|
+
await rm(file);
|
|
5725
|
+
await this.capture({ message: `chat ${this.repo.remote.repoId} (delete ${sessionId})` });
|
|
5726
|
+
this.pendingRemovalSessionIds.delete(sessionId);
|
|
5727
|
+
return true;
|
|
5728
|
+
}
|
|
5552
5729
|
async ensureGitignore() {
|
|
5553
5730
|
await mkdir(this.repo.hostDir, { recursive: true });
|
|
5554
5731
|
const target = path.join(this.repo.hostDir, ".gitignore");
|
|
@@ -5589,7 +5766,9 @@ var ClaudeRepo = class ClaudeRepo {
|
|
|
5589
5766
|
dirty: dirty ?? true,
|
|
5590
5767
|
now: Date.now()
|
|
5591
5768
|
}, config);
|
|
5592
|
-
if (step.push) this.capture({ message: `chat ${this.repo.remote.repoId}` })
|
|
5769
|
+
if (step.push) this.capture({ message: `chat ${this.repo.remote.repoId}` }).catch((err) => {
|
|
5770
|
+
console.error(`[ClaudeRepo] capture failed (${this.repo.remote.repoId}):`, err);
|
|
5771
|
+
});
|
|
5593
5772
|
if (this.watchTimer !== null || this.activeSessions.size > 0) this.armWatch(config, step.wakeAfterMs);
|
|
5594
5773
|
}
|
|
5595
5774
|
async readDirty() {
|
|
@@ -10851,7 +11030,7 @@ var JsonError$1 = class JsonError extends Error {
|
|
|
10851
11030
|
this.name = "JsonError";
|
|
10852
11031
|
}
|
|
10853
11032
|
};
|
|
10854
|
-
var ParseError$
|
|
11033
|
+
var ParseError$2 = class ParseError extends Error {
|
|
10855
11034
|
errors;
|
|
10856
11035
|
constructor(errors) {
|
|
10857
11036
|
super(errors.map(stringifyValidationError$1).join("; "));
|
|
@@ -10869,7 +11048,7 @@ function getSchemaUtils$1(schema) {
|
|
|
10869
11048
|
parseOrThrow: (raw, opts) => {
|
|
10870
11049
|
const parsed = schema.parse(raw, opts);
|
|
10871
11050
|
if (parsed.ok) return parsed.value;
|
|
10872
|
-
throw new ParseError$
|
|
11051
|
+
throw new ParseError$2(parsed.errors);
|
|
10873
11052
|
},
|
|
10874
11053
|
jsonOrThrow: (parsed, opts) => {
|
|
10875
11054
|
const raw = schema.json(parsed, opts);
|
|
@@ -12755,11 +12934,15 @@ const pluginAuthSchema = discriminatedUnion("type", [
|
|
|
12755
12934
|
pluginAuthClientCredentialsSchema,
|
|
12756
12935
|
pluginOAuthSchema
|
|
12757
12936
|
]).meta({ title: "PluginAuth" });
|
|
12937
|
+
const pluginMcpEnabledToolSchema = strictObject({ autoAllow: boolean$1().optional() });
|
|
12938
|
+
const pluginMcpEnabledToolsSchema = record$2(string$3().min(1), pluginMcpEnabledToolSchema).refine((tools) => Object.keys(tools).length > 0, { message: "enabledTools must declare at least one tool" }).meta({ title: "PluginMcpEnabledTools" });
|
|
12758
12939
|
const pluginMcpHttpServerSchema = strictObject({
|
|
12940
|
+
enabledTools: pluginMcpEnabledToolsSchema,
|
|
12759
12941
|
type: literal("http"),
|
|
12760
12942
|
url: url()
|
|
12761
12943
|
}).meta({ title: "PluginMcpHttpServer" });
|
|
12762
12944
|
const pluginMcpModuleServerSchema = strictObject({
|
|
12945
|
+
enabledTools: pluginMcpEnabledToolsSchema,
|
|
12763
12946
|
module: string$3().trim().min(1),
|
|
12764
12947
|
type: literal("module")
|
|
12765
12948
|
}).meta({ title: "PluginMcpModuleServer" });
|
|
@@ -12864,7 +13047,28 @@ const pluginRegistry = [
|
|
|
12864
13047
|
plugin_id: "linear"
|
|
12865
13048
|
},
|
|
12866
13049
|
{
|
|
12867
|
-
auth: {
|
|
13050
|
+
auth: {
|
|
13051
|
+
provider_id: "microsoft",
|
|
13052
|
+
scopes: ["Calendars.ReadWrite"],
|
|
13053
|
+
type: "oauth"
|
|
13054
|
+
},
|
|
13055
|
+
mcpServers: { microsoft_calendar: {
|
|
13056
|
+
enabledTools: {
|
|
13057
|
+
"accept-calendar-event": {},
|
|
13058
|
+
"cancel-calendar-event": {},
|
|
13059
|
+
"create-calendar-event": {},
|
|
13060
|
+
"decline-calendar-event": {},
|
|
13061
|
+
"delete-calendar-event": {},
|
|
13062
|
+
"get-calendar-event": { autoAllow: true },
|
|
13063
|
+
"get-calendar-view": { autoAllow: true },
|
|
13064
|
+
"list-calendar-events": { autoAllow: true },
|
|
13065
|
+
"list-calendars": { autoAllow: true },
|
|
13066
|
+
"tentatively-accept-calendar-event": {},
|
|
13067
|
+
"update-calendar-event": {}
|
|
13068
|
+
},
|
|
13069
|
+
type: "http",
|
|
13070
|
+
url: "https://mcp-microsoft-365.usecontextlayer.com/mcp"
|
|
13071
|
+
} },
|
|
12868
13072
|
mode: "dagster",
|
|
12869
13073
|
plugin_id: "microsoft_calendar"
|
|
12870
13074
|
},
|
|
@@ -12875,6 +13079,17 @@ const pluginRegistry = [
|
|
|
12875
13079
|
type: "oauth"
|
|
12876
13080
|
},
|
|
12877
13081
|
mcpServers: { microsoft_mail: {
|
|
13082
|
+
enabledTools: {
|
|
13083
|
+
"create-draft-email": {},
|
|
13084
|
+
"create-reply-draft": {},
|
|
13085
|
+
"get-mail-message": { autoAllow: true },
|
|
13086
|
+
"list-mail-folder-messages": { autoAllow: true },
|
|
13087
|
+
"list-mail-folders": { autoAllow: true },
|
|
13088
|
+
"list-mail-messages": { autoAllow: true },
|
|
13089
|
+
"reply-mail-message": {},
|
|
13090
|
+
"send-draft-message": {},
|
|
13091
|
+
"send-mail": {}
|
|
13092
|
+
},
|
|
12878
13093
|
type: "http",
|
|
12879
13094
|
url: "https://mcp-microsoft-365.usecontextlayer.com/mcp"
|
|
12880
13095
|
} },
|
|
@@ -12888,6 +13103,10 @@ const pluginRegistry = [
|
|
|
12888
13103
|
type: "oauth"
|
|
12889
13104
|
},
|
|
12890
13105
|
mcpServers: { microsoft_planner_premium: {
|
|
13106
|
+
enabledTools: {
|
|
13107
|
+
submit_operation_set: {},
|
|
13108
|
+
wait_for_operation_set: { autoAllow: true }
|
|
13109
|
+
},
|
|
12891
13110
|
module: "@usecontextlayer/mcp-microsoft-planner-premium",
|
|
12892
13111
|
type: "module"
|
|
12893
13112
|
} },
|
|
@@ -12989,6 +13208,13 @@ const providerTokenLeaseSchema = object$4({
|
|
|
12989
13208
|
accessTokenExpiresAt: datetime$2({ offset: true }).transform((value) => new Date(value)).nullable()
|
|
12990
13209
|
}).meta({ title: "ProviderTokenLease" });
|
|
12991
13210
|
|
|
13211
|
+
//#endregion
|
|
13212
|
+
//#region ../shared/dist/ctx-token-provider.mjs
|
|
13213
|
+
function ctxAuthHeaders(tokenProvider) {
|
|
13214
|
+
if (!tokenProvider) return {};
|
|
13215
|
+
return { headers: { Authorization: async () => `Bearer ${await tokenProvider()}` } };
|
|
13216
|
+
}
|
|
13217
|
+
|
|
12992
13218
|
//#endregion
|
|
12993
13219
|
//#region ../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/buffer_utils.js
|
|
12994
13220
|
const encoder = new TextEncoder();
|
|
@@ -14339,13 +14565,6 @@ function createDevJwtVerifier() {
|
|
|
14339
14565
|
};
|
|
14340
14566
|
}
|
|
14341
14567
|
|
|
14342
|
-
//#endregion
|
|
14343
|
-
//#region ../shared/dist/index.mjs
|
|
14344
|
-
function ctxAuthHeaders(tokenProvider) {
|
|
14345
|
-
if (!tokenProvider) return {};
|
|
14346
|
-
return { headers: { Authorization: async () => `Bearer ${await tokenProvider()}` } };
|
|
14347
|
-
}
|
|
14348
|
-
|
|
14349
14568
|
//#endregion
|
|
14350
14569
|
//#region ../slate-shared/dist/index.mjs
|
|
14351
14570
|
const INBOX_DOCS_TREE = {
|
|
@@ -20040,7 +20259,7 @@ var require_formdata = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
20040
20259
|
const { webidl } = require_webidl();
|
|
20041
20260
|
const nodeUtil$2 = __require("node:util");
|
|
20042
20261
|
const { runtimeFeatures } = require_runtime_features();
|
|
20043
|
-
const random = runtimeFeatures.has("crypto") ? __require("node:crypto").randomInt : (max) => Math.floor(Math.random() * max);
|
|
20262
|
+
const random$1 = runtimeFeatures.has("crypto") ? __require("node:crypto").randomInt : (max) => Math.floor(Math.random() * max);
|
|
20044
20263
|
var FormData = class FormData {
|
|
20045
20264
|
#state = [];
|
|
20046
20265
|
#boundary = null;
|
|
@@ -20140,7 +20359,7 @@ var require_formdata = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
20140
20359
|
static getFormDataBoundary(formData) {
|
|
20141
20360
|
const boundary = formData.#boundary;
|
|
20142
20361
|
if (boundary != null) return boundary;
|
|
20143
|
-
return formData.#boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, "0")}`;
|
|
20362
|
+
return formData.#boundary = `----formdata-undici-0${`${random$1(1e11)}`.padStart(11, "0")}`;
|
|
20144
20363
|
}
|
|
20145
20364
|
};
|
|
20146
20365
|
const { getFormDataState, setFormDataState, getFormDataBoundary } = FormData;
|
|
@@ -38629,7 +38848,7 @@ var JsonError = class JsonError extends Error {
|
|
|
38629
38848
|
this.name = "JsonError";
|
|
38630
38849
|
}
|
|
38631
38850
|
};
|
|
38632
|
-
var ParseError = class ParseError extends Error {
|
|
38851
|
+
var ParseError$1 = class ParseError extends Error {
|
|
38633
38852
|
errors;
|
|
38634
38853
|
constructor(errors) {
|
|
38635
38854
|
super(errors.map(stringifyValidationError).join("; "));
|
|
@@ -38647,7 +38866,7 @@ function getSchemaUtils(schema) {
|
|
|
38647
38866
|
parseOrThrow: (raw, opts) => {
|
|
38648
38867
|
const parsed = schema.parse(raw, opts);
|
|
38649
38868
|
if (parsed.ok) return parsed.value;
|
|
38650
|
-
throw new ParseError(parsed.errors);
|
|
38869
|
+
throw new ParseError$1(parsed.errors);
|
|
38651
38870
|
},
|
|
38652
38871
|
jsonOrThrow: (parsed, opts) => {
|
|
38653
38872
|
const raw = schema.json(parsed, opts);
|
|
@@ -40311,6 +40530,56 @@ const OAuthTokenRevocationRequestSchema = object$4({
|
|
|
40311
40530
|
//#region ../../node_modules/.pnpm/pkce-challenge@5.0.1/node_modules/pkce-challenge/dist/index.node.js
|
|
40312
40531
|
let crypto$2;
|
|
40313
40532
|
crypto$2 = globalThis.crypto?.webcrypto ?? globalThis.crypto ?? import("node:crypto").then((m) => m.webcrypto);
|
|
40533
|
+
/**
|
|
40534
|
+
* Creates an array of length `size` of random bytes
|
|
40535
|
+
* @param size
|
|
40536
|
+
* @returns Array of random ints (0 to 255)
|
|
40537
|
+
*/
|
|
40538
|
+
async function getRandomValues(size) {
|
|
40539
|
+
return (await crypto$2).getRandomValues(new Uint8Array(size));
|
|
40540
|
+
}
|
|
40541
|
+
/** Generate cryptographically strong random string
|
|
40542
|
+
* @param size The desired length of the string
|
|
40543
|
+
* @returns The random string
|
|
40544
|
+
*/
|
|
40545
|
+
async function random(size) {
|
|
40546
|
+
const mask = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
|
|
40547
|
+
const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % 66;
|
|
40548
|
+
let result = "";
|
|
40549
|
+
while (result.length < size) {
|
|
40550
|
+
const randomBytes = await getRandomValues(size - result.length);
|
|
40551
|
+
for (const randomByte of randomBytes) if (randomByte < evenDistCutoff) result += mask[randomByte % 66];
|
|
40552
|
+
}
|
|
40553
|
+
return result;
|
|
40554
|
+
}
|
|
40555
|
+
/** Generate a PKCE challenge verifier
|
|
40556
|
+
* @param length Length of the verifier
|
|
40557
|
+
* @returns A random verifier `length` characters long
|
|
40558
|
+
*/
|
|
40559
|
+
async function generateVerifier(length) {
|
|
40560
|
+
return await random(length);
|
|
40561
|
+
}
|
|
40562
|
+
/** Generate a PKCE code challenge from a code verifier
|
|
40563
|
+
* @param code_verifier
|
|
40564
|
+
* @returns The base64 url encoded code challenge
|
|
40565
|
+
*/
|
|
40566
|
+
async function generateChallenge(code_verifier) {
|
|
40567
|
+
const buffer = await (await crypto$2).subtle.digest("SHA-256", new TextEncoder().encode(code_verifier));
|
|
40568
|
+
return btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "");
|
|
40569
|
+
}
|
|
40570
|
+
/** Generate a PKCE challenge pair
|
|
40571
|
+
* @param length Length of the verifer (between 43-128). Defaults to 43.
|
|
40572
|
+
* @returns PKCE challenge pair
|
|
40573
|
+
*/
|
|
40574
|
+
async function pkceChallenge(length) {
|
|
40575
|
+
if (!length) length = 43;
|
|
40576
|
+
if (length < 43 || length > 128) throw `Expected a length between 43 and 128. Received ${length}.`;
|
|
40577
|
+
const verifier = await generateVerifier(length);
|
|
40578
|
+
return {
|
|
40579
|
+
code_verifier: verifier,
|
|
40580
|
+
code_challenge: await generateChallenge(verifier)
|
|
40581
|
+
};
|
|
40582
|
+
}
|
|
40314
40583
|
|
|
40315
40584
|
//#endregion
|
|
40316
40585
|
//#region ../../node_modules/.pnpm/@hono+mcp@0.3.1_@modelcontextprotocol+sdk@1.29.0_zod@4.4.3__hono-rate-limiter@0.5.3_hono@4.12.31__hono@4.12.31_zod@4.4.3/node_modules/@hono/mcp/dist/auth.mjs
|
|
@@ -40836,6 +41105,7 @@ const InitializedNotificationSchema = NotificationSchema.extend({
|
|
|
40836
41105
|
method: literal("notifications/initialized"),
|
|
40837
41106
|
params: NotificationsParamsSchema.optional()
|
|
40838
41107
|
});
|
|
41108
|
+
const isInitializedNotification = (value) => InitializedNotificationSchema.safeParse(value).success;
|
|
40839
41109
|
/**
|
|
40840
41110
|
* A ping, issued by either the server or the client, to check that the other party is still alive. The receiver must promptly respond, or else may be disconnected.
|
|
40841
41111
|
*/
|
|
@@ -54872,6 +55142,1978 @@ var AjvJsonSchemaValidator = class {
|
|
|
54872
55142
|
}
|
|
54873
55143
|
};
|
|
54874
55144
|
|
|
55145
|
+
//#endregion
|
|
55146
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/client.js
|
|
55147
|
+
/**
|
|
55148
|
+
* Experimental client task features for MCP SDK.
|
|
55149
|
+
* WARNING: These APIs are experimental and may change without notice.
|
|
55150
|
+
*
|
|
55151
|
+
* @experimental
|
|
55152
|
+
*/
|
|
55153
|
+
/**
|
|
55154
|
+
* Experimental task features for MCP clients.
|
|
55155
|
+
*
|
|
55156
|
+
* Access via `client.experimental.tasks`:
|
|
55157
|
+
* ```typescript
|
|
55158
|
+
* const stream = client.experimental.tasks.callToolStream({ name: 'tool', arguments: {} });
|
|
55159
|
+
* const task = await client.experimental.tasks.getTask(taskId);
|
|
55160
|
+
* ```
|
|
55161
|
+
*
|
|
55162
|
+
* @experimental
|
|
55163
|
+
*/
|
|
55164
|
+
var ExperimentalClientTasks = class {
|
|
55165
|
+
constructor(_client) {
|
|
55166
|
+
this._client = _client;
|
|
55167
|
+
}
|
|
55168
|
+
/**
|
|
55169
|
+
* Calls a tool and returns an AsyncGenerator that yields response messages.
|
|
55170
|
+
* The generator is guaranteed to end with either a 'result' or 'error' message.
|
|
55171
|
+
*
|
|
55172
|
+
* This method provides streaming access to tool execution, allowing you to
|
|
55173
|
+
* observe intermediate task status updates for long-running tool calls.
|
|
55174
|
+
* Automatically validates structured output if the tool has an outputSchema.
|
|
55175
|
+
*
|
|
55176
|
+
* @example
|
|
55177
|
+
* ```typescript
|
|
55178
|
+
* const stream = client.experimental.tasks.callToolStream({ name: 'myTool', arguments: {} });
|
|
55179
|
+
* for await (const message of stream) {
|
|
55180
|
+
* switch (message.type) {
|
|
55181
|
+
* case 'taskCreated':
|
|
55182
|
+
* console.log('Tool execution started:', message.task.taskId);
|
|
55183
|
+
* break;
|
|
55184
|
+
* case 'taskStatus':
|
|
55185
|
+
* console.log('Tool status:', message.task.status);
|
|
55186
|
+
* break;
|
|
55187
|
+
* case 'result':
|
|
55188
|
+
* console.log('Tool result:', message.result);
|
|
55189
|
+
* break;
|
|
55190
|
+
* case 'error':
|
|
55191
|
+
* console.error('Tool error:', message.error);
|
|
55192
|
+
* break;
|
|
55193
|
+
* }
|
|
55194
|
+
* }
|
|
55195
|
+
* ```
|
|
55196
|
+
*
|
|
55197
|
+
* @param params - Tool call parameters (name and arguments)
|
|
55198
|
+
* @param resultSchema - Zod schema for validating the result (defaults to CallToolResultSchema)
|
|
55199
|
+
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
|
55200
|
+
* @returns AsyncGenerator that yields ResponseMessage objects
|
|
55201
|
+
*
|
|
55202
|
+
* @experimental
|
|
55203
|
+
*/
|
|
55204
|
+
async *callToolStream(params, resultSchema = CallToolResultSchema, options) {
|
|
55205
|
+
const clientInternal = this._client;
|
|
55206
|
+
const optionsWithTask = {
|
|
55207
|
+
...options,
|
|
55208
|
+
task: options?.task ?? (clientInternal.isToolTask(params.name) ? {} : void 0)
|
|
55209
|
+
};
|
|
55210
|
+
const stream = clientInternal.requestStream({
|
|
55211
|
+
method: "tools/call",
|
|
55212
|
+
params
|
|
55213
|
+
}, resultSchema, optionsWithTask);
|
|
55214
|
+
const validator = clientInternal.getToolOutputValidator(params.name);
|
|
55215
|
+
for await (const message of stream) {
|
|
55216
|
+
if (message.type === "result" && validator) {
|
|
55217
|
+
const result = message.result;
|
|
55218
|
+
if (!result.structuredContent && !result.isError) {
|
|
55219
|
+
yield {
|
|
55220
|
+
type: "error",
|
|
55221
|
+
error: new McpError(ErrorCode.InvalidRequest, `Tool ${params.name} has an output schema but did not return structured content`)
|
|
55222
|
+
};
|
|
55223
|
+
return;
|
|
55224
|
+
}
|
|
55225
|
+
if (result.structuredContent) try {
|
|
55226
|
+
const validationResult = validator(result.structuredContent);
|
|
55227
|
+
if (!validationResult.valid) {
|
|
55228
|
+
yield {
|
|
55229
|
+
type: "error",
|
|
55230
|
+
error: new McpError(ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validationResult.errorMessage}`)
|
|
55231
|
+
};
|
|
55232
|
+
return;
|
|
55233
|
+
}
|
|
55234
|
+
} catch (error) {
|
|
55235
|
+
if (error instanceof McpError) {
|
|
55236
|
+
yield {
|
|
55237
|
+
type: "error",
|
|
55238
|
+
error
|
|
55239
|
+
};
|
|
55240
|
+
return;
|
|
55241
|
+
}
|
|
55242
|
+
yield {
|
|
55243
|
+
type: "error",
|
|
55244
|
+
error: new McpError(ErrorCode.InvalidParams, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`)
|
|
55245
|
+
};
|
|
55246
|
+
return;
|
|
55247
|
+
}
|
|
55248
|
+
}
|
|
55249
|
+
yield message;
|
|
55250
|
+
}
|
|
55251
|
+
}
|
|
55252
|
+
/**
|
|
55253
|
+
* Gets the current status of a task.
|
|
55254
|
+
*
|
|
55255
|
+
* @param taskId - The task identifier
|
|
55256
|
+
* @param options - Optional request options
|
|
55257
|
+
* @returns The task status
|
|
55258
|
+
*
|
|
55259
|
+
* @experimental
|
|
55260
|
+
*/
|
|
55261
|
+
async getTask(taskId, options) {
|
|
55262
|
+
return this._client.getTask({ taskId }, options);
|
|
55263
|
+
}
|
|
55264
|
+
/**
|
|
55265
|
+
* Retrieves the result of a completed task.
|
|
55266
|
+
*
|
|
55267
|
+
* @param taskId - The task identifier
|
|
55268
|
+
* @param resultSchema - Zod schema for validating the result
|
|
55269
|
+
* @param options - Optional request options
|
|
55270
|
+
* @returns The task result
|
|
55271
|
+
*
|
|
55272
|
+
* @experimental
|
|
55273
|
+
*/
|
|
55274
|
+
async getTaskResult(taskId, resultSchema, options) {
|
|
55275
|
+
return this._client.getTaskResult({ taskId }, resultSchema, options);
|
|
55276
|
+
}
|
|
55277
|
+
/**
|
|
55278
|
+
* Lists tasks with optional pagination.
|
|
55279
|
+
*
|
|
55280
|
+
* @param cursor - Optional pagination cursor
|
|
55281
|
+
* @param options - Optional request options
|
|
55282
|
+
* @returns List of tasks with optional next cursor
|
|
55283
|
+
*
|
|
55284
|
+
* @experimental
|
|
55285
|
+
*/
|
|
55286
|
+
async listTasks(cursor, options) {
|
|
55287
|
+
return this._client.listTasks(cursor ? { cursor } : void 0, options);
|
|
55288
|
+
}
|
|
55289
|
+
/**
|
|
55290
|
+
* Cancels a running task.
|
|
55291
|
+
*
|
|
55292
|
+
* @param taskId - The task identifier
|
|
55293
|
+
* @param options - Optional request options
|
|
55294
|
+
*
|
|
55295
|
+
* @experimental
|
|
55296
|
+
*/
|
|
55297
|
+
async cancelTask(taskId, options) {
|
|
55298
|
+
return this._client.cancelTask({ taskId }, options);
|
|
55299
|
+
}
|
|
55300
|
+
/**
|
|
55301
|
+
* Sends a request and returns an AsyncGenerator that yields response messages.
|
|
55302
|
+
* The generator is guaranteed to end with either a 'result' or 'error' message.
|
|
55303
|
+
*
|
|
55304
|
+
* This method provides streaming access to request processing, allowing you to
|
|
55305
|
+
* observe intermediate task status updates for task-augmented requests.
|
|
55306
|
+
*
|
|
55307
|
+
* @param request - The request to send
|
|
55308
|
+
* @param resultSchema - Zod schema for validating the result
|
|
55309
|
+
* @param options - Optional request options (timeout, signal, task creation params, etc.)
|
|
55310
|
+
* @returns AsyncGenerator that yields ResponseMessage objects
|
|
55311
|
+
*
|
|
55312
|
+
* @experimental
|
|
55313
|
+
*/
|
|
55314
|
+
requestStream(request, resultSchema, options) {
|
|
55315
|
+
return this._client.requestStream(request, resultSchema, options);
|
|
55316
|
+
}
|
|
55317
|
+
};
|
|
55318
|
+
|
|
55319
|
+
//#endregion
|
|
55320
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
|
|
55321
|
+
/**
|
|
55322
|
+
* Experimental task capability assertion helpers.
|
|
55323
|
+
* WARNING: These APIs are experimental and may change without notice.
|
|
55324
|
+
*
|
|
55325
|
+
* @experimental
|
|
55326
|
+
*/
|
|
55327
|
+
/**
|
|
55328
|
+
* Asserts that task creation is supported for tools/call.
|
|
55329
|
+
* Used by Client.assertTaskCapability and Server.assertTaskHandlerCapability.
|
|
55330
|
+
*
|
|
55331
|
+
* @param requests - The task requests capability object
|
|
55332
|
+
* @param method - The method being checked
|
|
55333
|
+
* @param entityName - 'Server' or 'Client' for error messages
|
|
55334
|
+
* @throws Error if the capability is not supported
|
|
55335
|
+
*
|
|
55336
|
+
* @experimental
|
|
55337
|
+
*/
|
|
55338
|
+
function assertToolsCallTaskCapability(requests, method, entityName) {
|
|
55339
|
+
if (!requests) throw new Error(`${entityName} does not support task creation (required for ${method})`);
|
|
55340
|
+
switch (method) {
|
|
55341
|
+
case "tools/call":
|
|
55342
|
+
if (!requests.tools?.call) throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`);
|
|
55343
|
+
break;
|
|
55344
|
+
default: break;
|
|
55345
|
+
}
|
|
55346
|
+
}
|
|
55347
|
+
/**
|
|
55348
|
+
* Asserts that task creation is supported for sampling/createMessage or elicitation/create.
|
|
55349
|
+
* Used by Server.assertTaskCapability and Client.assertTaskHandlerCapability.
|
|
55350
|
+
*
|
|
55351
|
+
* @param requests - The task requests capability object
|
|
55352
|
+
* @param method - The method being checked
|
|
55353
|
+
* @param entityName - 'Server' or 'Client' for error messages
|
|
55354
|
+
* @throws Error if the capability is not supported
|
|
55355
|
+
*
|
|
55356
|
+
* @experimental
|
|
55357
|
+
*/
|
|
55358
|
+
function assertClientRequestTaskCapability(requests, method, entityName) {
|
|
55359
|
+
if (!requests) throw new Error(`${entityName} does not support task creation (required for ${method})`);
|
|
55360
|
+
switch (method) {
|
|
55361
|
+
case "sampling/createMessage":
|
|
55362
|
+
if (!requests.sampling?.createMessage) throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
|
|
55363
|
+
break;
|
|
55364
|
+
case "elicitation/create":
|
|
55365
|
+
if (!requests.elicitation?.create) throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`);
|
|
55366
|
+
break;
|
|
55367
|
+
default: break;
|
|
55368
|
+
}
|
|
55369
|
+
}
|
|
55370
|
+
|
|
55371
|
+
//#endregion
|
|
55372
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/client/index.js
|
|
55373
|
+
/**
|
|
55374
|
+
* Elicitation default application helper. Applies defaults to the data based on the schema.
|
|
55375
|
+
*
|
|
55376
|
+
* @param schema - The schema to apply defaults to.
|
|
55377
|
+
* @param data - The data to apply defaults to.
|
|
55378
|
+
*/
|
|
55379
|
+
function applyElicitationDefaults(schema, data) {
|
|
55380
|
+
if (!schema || data === null || typeof data !== "object") return;
|
|
55381
|
+
if (schema.type === "object" && schema.properties && typeof schema.properties === "object") {
|
|
55382
|
+
const obj = data;
|
|
55383
|
+
const props = schema.properties;
|
|
55384
|
+
for (const key of Object.keys(props)) {
|
|
55385
|
+
const propSchema = props[key];
|
|
55386
|
+
if (obj[key] === void 0 && Object.prototype.hasOwnProperty.call(propSchema, "default")) obj[key] = propSchema.default;
|
|
55387
|
+
if (obj[key] !== void 0) applyElicitationDefaults(propSchema, obj[key]);
|
|
55388
|
+
}
|
|
55389
|
+
}
|
|
55390
|
+
if (Array.isArray(schema.anyOf)) {
|
|
55391
|
+
for (const sub of schema.anyOf) if (typeof sub !== "boolean") applyElicitationDefaults(sub, data);
|
|
55392
|
+
}
|
|
55393
|
+
if (Array.isArray(schema.oneOf)) {
|
|
55394
|
+
for (const sub of schema.oneOf) if (typeof sub !== "boolean") applyElicitationDefaults(sub, data);
|
|
55395
|
+
}
|
|
55396
|
+
}
|
|
55397
|
+
/**
|
|
55398
|
+
* Determines which elicitation modes are supported based on declared client capabilities.
|
|
55399
|
+
*
|
|
55400
|
+
* According to the spec:
|
|
55401
|
+
* - An empty elicitation capability object defaults to form mode support (backwards compatibility)
|
|
55402
|
+
* - URL mode is only supported if explicitly declared
|
|
55403
|
+
*
|
|
55404
|
+
* @param capabilities - The client's elicitation capabilities
|
|
55405
|
+
* @returns An object indicating which modes are supported
|
|
55406
|
+
*/
|
|
55407
|
+
function getSupportedElicitationModes(capabilities) {
|
|
55408
|
+
if (!capabilities) return {
|
|
55409
|
+
supportsFormMode: false,
|
|
55410
|
+
supportsUrlMode: false
|
|
55411
|
+
};
|
|
55412
|
+
const hasFormCapability = capabilities.form !== void 0;
|
|
55413
|
+
const hasUrlCapability = capabilities.url !== void 0;
|
|
55414
|
+
return {
|
|
55415
|
+
supportsFormMode: hasFormCapability || !hasFormCapability && !hasUrlCapability,
|
|
55416
|
+
supportsUrlMode: hasUrlCapability
|
|
55417
|
+
};
|
|
55418
|
+
}
|
|
55419
|
+
/**
|
|
55420
|
+
* An MCP client on top of a pluggable transport.
|
|
55421
|
+
*
|
|
55422
|
+
* The client will automatically begin the initialization flow with the server when connect() is called.
|
|
55423
|
+
*
|
|
55424
|
+
* To use with custom types, extend the base Request/Notification/Result types and pass them as type parameters:
|
|
55425
|
+
*
|
|
55426
|
+
* ```typescript
|
|
55427
|
+
* // Custom schemas
|
|
55428
|
+
* const CustomRequestSchema = RequestSchema.extend({...})
|
|
55429
|
+
* const CustomNotificationSchema = NotificationSchema.extend({...})
|
|
55430
|
+
* const CustomResultSchema = ResultSchema.extend({...})
|
|
55431
|
+
*
|
|
55432
|
+
* // Type aliases
|
|
55433
|
+
* type CustomRequest = z.infer<typeof CustomRequestSchema>
|
|
55434
|
+
* type CustomNotification = z.infer<typeof CustomNotificationSchema>
|
|
55435
|
+
* type CustomResult = z.infer<typeof CustomResultSchema>
|
|
55436
|
+
*
|
|
55437
|
+
* // Create typed client
|
|
55438
|
+
* const client = new Client<CustomRequest, CustomNotification, CustomResult>({
|
|
55439
|
+
* name: "CustomClient",
|
|
55440
|
+
* version: "1.0.0"
|
|
55441
|
+
* })
|
|
55442
|
+
* ```
|
|
55443
|
+
*/
|
|
55444
|
+
var Client = class extends Protocol {
|
|
55445
|
+
/**
|
|
55446
|
+
* Initializes this client with the given name and version information.
|
|
55447
|
+
*/
|
|
55448
|
+
constructor(_clientInfo, options) {
|
|
55449
|
+
super(options);
|
|
55450
|
+
this._clientInfo = _clientInfo;
|
|
55451
|
+
this._cachedToolOutputValidators = /* @__PURE__ */ new Map();
|
|
55452
|
+
this._cachedKnownTaskTools = /* @__PURE__ */ new Set();
|
|
55453
|
+
this._cachedRequiredTaskTools = /* @__PURE__ */ new Set();
|
|
55454
|
+
this._listChangedDebounceTimers = /* @__PURE__ */ new Map();
|
|
55455
|
+
this._capabilities = options?.capabilities ?? {};
|
|
55456
|
+
this._jsonSchemaValidator = options?.jsonSchemaValidator ?? new AjvJsonSchemaValidator();
|
|
55457
|
+
if (options?.listChanged) this._pendingListChangedConfig = options.listChanged;
|
|
55458
|
+
}
|
|
55459
|
+
/**
|
|
55460
|
+
* Set up handlers for list changed notifications based on config and server capabilities.
|
|
55461
|
+
* This should only be called after initialization when server capabilities are known.
|
|
55462
|
+
* Handlers are silently skipped if the server doesn't advertise the corresponding listChanged capability.
|
|
55463
|
+
* @internal
|
|
55464
|
+
*/
|
|
55465
|
+
_setupListChangedHandlers(config) {
|
|
55466
|
+
if (config.tools && this._serverCapabilities?.tools?.listChanged) this._setupListChangedHandler("tools", ToolListChangedNotificationSchema, config.tools, async () => {
|
|
55467
|
+
return (await this.listTools()).tools;
|
|
55468
|
+
});
|
|
55469
|
+
if (config.prompts && this._serverCapabilities?.prompts?.listChanged) this._setupListChangedHandler("prompts", PromptListChangedNotificationSchema, config.prompts, async () => {
|
|
55470
|
+
return (await this.listPrompts()).prompts;
|
|
55471
|
+
});
|
|
55472
|
+
if (config.resources && this._serverCapabilities?.resources?.listChanged) this._setupListChangedHandler("resources", ResourceListChangedNotificationSchema, config.resources, async () => {
|
|
55473
|
+
return (await this.listResources()).resources;
|
|
55474
|
+
});
|
|
55475
|
+
}
|
|
55476
|
+
/**
|
|
55477
|
+
* Access experimental features.
|
|
55478
|
+
*
|
|
55479
|
+
* WARNING: These APIs are experimental and may change without notice.
|
|
55480
|
+
*
|
|
55481
|
+
* @experimental
|
|
55482
|
+
*/
|
|
55483
|
+
get experimental() {
|
|
55484
|
+
if (!this._experimental) this._experimental = { tasks: new ExperimentalClientTasks(this) };
|
|
55485
|
+
return this._experimental;
|
|
55486
|
+
}
|
|
55487
|
+
/**
|
|
55488
|
+
* Registers new capabilities. This can only be called before connecting to a transport.
|
|
55489
|
+
*
|
|
55490
|
+
* The new capabilities will be merged with any existing capabilities previously given (e.g., at initialization).
|
|
55491
|
+
*/
|
|
55492
|
+
registerCapabilities(capabilities) {
|
|
55493
|
+
if (this.transport) throw new Error("Cannot register capabilities after connecting to transport");
|
|
55494
|
+
this._capabilities = mergeCapabilities(this._capabilities, capabilities);
|
|
55495
|
+
}
|
|
55496
|
+
/**
|
|
55497
|
+
* Override request handler registration to enforce client-side validation for elicitation.
|
|
55498
|
+
*/
|
|
55499
|
+
setRequestHandler(requestSchema, handler) {
|
|
55500
|
+
const methodSchema = getObjectShape(requestSchema)?.method;
|
|
55501
|
+
if (!methodSchema) throw new Error("Schema is missing a method literal");
|
|
55502
|
+
let methodValue;
|
|
55503
|
+
if (isZ4Schema(methodSchema)) {
|
|
55504
|
+
const v4Schema = methodSchema;
|
|
55505
|
+
methodValue = (v4Schema._zod?.def)?.value ?? v4Schema.value;
|
|
55506
|
+
} else {
|
|
55507
|
+
const v3Schema = methodSchema;
|
|
55508
|
+
methodValue = v3Schema._def?.value ?? v3Schema.value;
|
|
55509
|
+
}
|
|
55510
|
+
if (typeof methodValue !== "string") throw new Error("Schema method literal must be a string");
|
|
55511
|
+
const method = methodValue;
|
|
55512
|
+
if (method === "elicitation/create") {
|
|
55513
|
+
const wrappedHandler = async (request, extra) => {
|
|
55514
|
+
const validatedRequest = safeParse(ElicitRequestSchema, request);
|
|
55515
|
+
if (!validatedRequest.success) {
|
|
55516
|
+
const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
55517
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation request: ${errorMessage}`);
|
|
55518
|
+
}
|
|
55519
|
+
const { params } = validatedRequest.data;
|
|
55520
|
+
params.mode = params.mode ?? "form";
|
|
55521
|
+
const { supportsFormMode, supportsUrlMode } = getSupportedElicitationModes(this._capabilities.elicitation);
|
|
55522
|
+
if (params.mode === "form" && !supportsFormMode) throw new McpError(ErrorCode.InvalidParams, "Client does not support form-mode elicitation requests");
|
|
55523
|
+
if (params.mode === "url" && !supportsUrlMode) throw new McpError(ErrorCode.InvalidParams, "Client does not support URL-mode elicitation requests");
|
|
55524
|
+
const result = await Promise.resolve(handler(request, extra));
|
|
55525
|
+
if (params.task) {
|
|
55526
|
+
const taskValidationResult = safeParse(CreateTaskResultSchema, result);
|
|
55527
|
+
if (!taskValidationResult.success) {
|
|
55528
|
+
const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
|
|
55529
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
|
55530
|
+
}
|
|
55531
|
+
return taskValidationResult.data;
|
|
55532
|
+
}
|
|
55533
|
+
const validationResult = safeParse(ElicitResultSchema, result);
|
|
55534
|
+
if (!validationResult.success) {
|
|
55535
|
+
const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
55536
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid elicitation result: ${errorMessage}`);
|
|
55537
|
+
}
|
|
55538
|
+
const validatedResult = validationResult.data;
|
|
55539
|
+
const requestedSchema = params.mode === "form" ? params.requestedSchema : void 0;
|
|
55540
|
+
if (params.mode === "form" && validatedResult.action === "accept" && validatedResult.content && requestedSchema) {
|
|
55541
|
+
if (this._capabilities.elicitation?.form?.applyDefaults) try {
|
|
55542
|
+
applyElicitationDefaults(requestedSchema, validatedResult.content);
|
|
55543
|
+
} catch {}
|
|
55544
|
+
}
|
|
55545
|
+
return validatedResult;
|
|
55546
|
+
};
|
|
55547
|
+
return super.setRequestHandler(requestSchema, wrappedHandler);
|
|
55548
|
+
}
|
|
55549
|
+
if (method === "sampling/createMessage") {
|
|
55550
|
+
const wrappedHandler = async (request, extra) => {
|
|
55551
|
+
const validatedRequest = safeParse(CreateMessageRequestSchema, request);
|
|
55552
|
+
if (!validatedRequest.success) {
|
|
55553
|
+
const errorMessage = validatedRequest.error instanceof Error ? validatedRequest.error.message : String(validatedRequest.error);
|
|
55554
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid sampling request: ${errorMessage}`);
|
|
55555
|
+
}
|
|
55556
|
+
const { params } = validatedRequest.data;
|
|
55557
|
+
const result = await Promise.resolve(handler(request, extra));
|
|
55558
|
+
if (params.task) {
|
|
55559
|
+
const taskValidationResult = safeParse(CreateTaskResultSchema, result);
|
|
55560
|
+
if (!taskValidationResult.success) {
|
|
55561
|
+
const errorMessage = taskValidationResult.error instanceof Error ? taskValidationResult.error.message : String(taskValidationResult.error);
|
|
55562
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid task creation result: ${errorMessage}`);
|
|
55563
|
+
}
|
|
55564
|
+
return taskValidationResult.data;
|
|
55565
|
+
}
|
|
55566
|
+
const validationResult = safeParse(params.tools || params.toolChoice ? CreateMessageResultWithToolsSchema : CreateMessageResultSchema, result);
|
|
55567
|
+
if (!validationResult.success) {
|
|
55568
|
+
const errorMessage = validationResult.error instanceof Error ? validationResult.error.message : String(validationResult.error);
|
|
55569
|
+
throw new McpError(ErrorCode.InvalidParams, `Invalid sampling result: ${errorMessage}`);
|
|
55570
|
+
}
|
|
55571
|
+
return validationResult.data;
|
|
55572
|
+
};
|
|
55573
|
+
return super.setRequestHandler(requestSchema, wrappedHandler);
|
|
55574
|
+
}
|
|
55575
|
+
return super.setRequestHandler(requestSchema, handler);
|
|
55576
|
+
}
|
|
55577
|
+
assertCapability(capability, method) {
|
|
55578
|
+
if (!this._serverCapabilities?.[capability]) throw new Error(`Server does not support ${capability} (required for ${method})`);
|
|
55579
|
+
}
|
|
55580
|
+
async connect(transport, options) {
|
|
55581
|
+
await super.connect(transport);
|
|
55582
|
+
if (transport.sessionId !== void 0) return;
|
|
55583
|
+
try {
|
|
55584
|
+
const result = await this.request({
|
|
55585
|
+
method: "initialize",
|
|
55586
|
+
params: {
|
|
55587
|
+
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
55588
|
+
capabilities: this._capabilities,
|
|
55589
|
+
clientInfo: this._clientInfo
|
|
55590
|
+
}
|
|
55591
|
+
}, InitializeResultSchema, options);
|
|
55592
|
+
if (result === void 0) throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
55593
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(result.protocolVersion)) throw new Error(`Server's protocol version is not supported: ${result.protocolVersion}`);
|
|
55594
|
+
this._serverCapabilities = result.capabilities;
|
|
55595
|
+
this._serverVersion = result.serverInfo;
|
|
55596
|
+
if (transport.setProtocolVersion) transport.setProtocolVersion(result.protocolVersion);
|
|
55597
|
+
this._instructions = result.instructions;
|
|
55598
|
+
await this.notification({ method: "notifications/initialized" });
|
|
55599
|
+
if (this._pendingListChangedConfig) {
|
|
55600
|
+
this._setupListChangedHandlers(this._pendingListChangedConfig);
|
|
55601
|
+
this._pendingListChangedConfig = void 0;
|
|
55602
|
+
}
|
|
55603
|
+
} catch (error) {
|
|
55604
|
+
this.close();
|
|
55605
|
+
throw error;
|
|
55606
|
+
}
|
|
55607
|
+
}
|
|
55608
|
+
/**
|
|
55609
|
+
* After initialization has completed, this will be populated with the server's reported capabilities.
|
|
55610
|
+
*/
|
|
55611
|
+
getServerCapabilities() {
|
|
55612
|
+
return this._serverCapabilities;
|
|
55613
|
+
}
|
|
55614
|
+
/**
|
|
55615
|
+
* After initialization has completed, this will be populated with information about the server's name and version.
|
|
55616
|
+
*/
|
|
55617
|
+
getServerVersion() {
|
|
55618
|
+
return this._serverVersion;
|
|
55619
|
+
}
|
|
55620
|
+
/**
|
|
55621
|
+
* After initialization has completed, this may be populated with information about the server's instructions.
|
|
55622
|
+
*/
|
|
55623
|
+
getInstructions() {
|
|
55624
|
+
return this._instructions;
|
|
55625
|
+
}
|
|
55626
|
+
assertCapabilityForMethod(method) {
|
|
55627
|
+
switch (method) {
|
|
55628
|
+
case "logging/setLevel":
|
|
55629
|
+
if (!this._serverCapabilities?.logging) throw new Error(`Server does not support logging (required for ${method})`);
|
|
55630
|
+
break;
|
|
55631
|
+
case "prompts/get":
|
|
55632
|
+
case "prompts/list":
|
|
55633
|
+
if (!this._serverCapabilities?.prompts) throw new Error(`Server does not support prompts (required for ${method})`);
|
|
55634
|
+
break;
|
|
55635
|
+
case "resources/list":
|
|
55636
|
+
case "resources/templates/list":
|
|
55637
|
+
case "resources/read":
|
|
55638
|
+
case "resources/subscribe":
|
|
55639
|
+
case "resources/unsubscribe":
|
|
55640
|
+
if (!this._serverCapabilities?.resources) throw new Error(`Server does not support resources (required for ${method})`);
|
|
55641
|
+
if (method === "resources/subscribe" && !this._serverCapabilities.resources.subscribe) throw new Error(`Server does not support resource subscriptions (required for ${method})`);
|
|
55642
|
+
break;
|
|
55643
|
+
case "tools/call":
|
|
55644
|
+
case "tools/list":
|
|
55645
|
+
if (!this._serverCapabilities?.tools) throw new Error(`Server does not support tools (required for ${method})`);
|
|
55646
|
+
break;
|
|
55647
|
+
case "completion/complete":
|
|
55648
|
+
if (!this._serverCapabilities?.completions) throw new Error(`Server does not support completions (required for ${method})`);
|
|
55649
|
+
break;
|
|
55650
|
+
case "initialize": break;
|
|
55651
|
+
case "ping": break;
|
|
55652
|
+
}
|
|
55653
|
+
}
|
|
55654
|
+
assertNotificationCapability(method) {
|
|
55655
|
+
switch (method) {
|
|
55656
|
+
case "notifications/roots/list_changed":
|
|
55657
|
+
if (!this._capabilities.roots?.listChanged) throw new Error(`Client does not support roots list changed notifications (required for ${method})`);
|
|
55658
|
+
break;
|
|
55659
|
+
case "notifications/initialized": break;
|
|
55660
|
+
case "notifications/cancelled": break;
|
|
55661
|
+
case "notifications/progress": break;
|
|
55662
|
+
}
|
|
55663
|
+
}
|
|
55664
|
+
assertRequestHandlerCapability(method) {
|
|
55665
|
+
if (!this._capabilities) return;
|
|
55666
|
+
switch (method) {
|
|
55667
|
+
case "sampling/createMessage":
|
|
55668
|
+
if (!this._capabilities.sampling) throw new Error(`Client does not support sampling capability (required for ${method})`);
|
|
55669
|
+
break;
|
|
55670
|
+
case "elicitation/create":
|
|
55671
|
+
if (!this._capabilities.elicitation) throw new Error(`Client does not support elicitation capability (required for ${method})`);
|
|
55672
|
+
break;
|
|
55673
|
+
case "roots/list":
|
|
55674
|
+
if (!this._capabilities.roots) throw new Error(`Client does not support roots capability (required for ${method})`);
|
|
55675
|
+
break;
|
|
55676
|
+
case "tasks/get":
|
|
55677
|
+
case "tasks/list":
|
|
55678
|
+
case "tasks/result":
|
|
55679
|
+
case "tasks/cancel":
|
|
55680
|
+
if (!this._capabilities.tasks) throw new Error(`Client does not support tasks capability (required for ${method})`);
|
|
55681
|
+
break;
|
|
55682
|
+
case "ping": break;
|
|
55683
|
+
}
|
|
55684
|
+
}
|
|
55685
|
+
assertTaskCapability(method) {
|
|
55686
|
+
assertToolsCallTaskCapability(this._serverCapabilities?.tasks?.requests, method, "Server");
|
|
55687
|
+
}
|
|
55688
|
+
assertTaskHandlerCapability(method) {
|
|
55689
|
+
if (!this._capabilities) return;
|
|
55690
|
+
assertClientRequestTaskCapability(this._capabilities.tasks?.requests, method, "Client");
|
|
55691
|
+
}
|
|
55692
|
+
async ping(options) {
|
|
55693
|
+
return this.request({ method: "ping" }, EmptyResultSchema, options);
|
|
55694
|
+
}
|
|
55695
|
+
async complete(params, options) {
|
|
55696
|
+
return this.request({
|
|
55697
|
+
method: "completion/complete",
|
|
55698
|
+
params
|
|
55699
|
+
}, CompleteResultSchema, options);
|
|
55700
|
+
}
|
|
55701
|
+
async setLoggingLevel(level, options) {
|
|
55702
|
+
return this.request({
|
|
55703
|
+
method: "logging/setLevel",
|
|
55704
|
+
params: { level }
|
|
55705
|
+
}, EmptyResultSchema, options);
|
|
55706
|
+
}
|
|
55707
|
+
async getPrompt(params, options) {
|
|
55708
|
+
return this.request({
|
|
55709
|
+
method: "prompts/get",
|
|
55710
|
+
params
|
|
55711
|
+
}, GetPromptResultSchema, options);
|
|
55712
|
+
}
|
|
55713
|
+
async listPrompts(params, options) {
|
|
55714
|
+
return this.request({
|
|
55715
|
+
method: "prompts/list",
|
|
55716
|
+
params
|
|
55717
|
+
}, ListPromptsResultSchema, options);
|
|
55718
|
+
}
|
|
55719
|
+
async listResources(params, options) {
|
|
55720
|
+
return this.request({
|
|
55721
|
+
method: "resources/list",
|
|
55722
|
+
params
|
|
55723
|
+
}, ListResourcesResultSchema, options);
|
|
55724
|
+
}
|
|
55725
|
+
async listResourceTemplates(params, options) {
|
|
55726
|
+
return this.request({
|
|
55727
|
+
method: "resources/templates/list",
|
|
55728
|
+
params
|
|
55729
|
+
}, ListResourceTemplatesResultSchema, options);
|
|
55730
|
+
}
|
|
55731
|
+
async readResource(params, options) {
|
|
55732
|
+
return this.request({
|
|
55733
|
+
method: "resources/read",
|
|
55734
|
+
params
|
|
55735
|
+
}, ReadResourceResultSchema, options);
|
|
55736
|
+
}
|
|
55737
|
+
async subscribeResource(params, options) {
|
|
55738
|
+
return this.request({
|
|
55739
|
+
method: "resources/subscribe",
|
|
55740
|
+
params
|
|
55741
|
+
}, EmptyResultSchema, options);
|
|
55742
|
+
}
|
|
55743
|
+
async unsubscribeResource(params, options) {
|
|
55744
|
+
return this.request({
|
|
55745
|
+
method: "resources/unsubscribe",
|
|
55746
|
+
params
|
|
55747
|
+
}, EmptyResultSchema, options);
|
|
55748
|
+
}
|
|
55749
|
+
/**
|
|
55750
|
+
* Calls a tool and waits for the result. Automatically validates structured output if the tool has an outputSchema.
|
|
55751
|
+
*
|
|
55752
|
+
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
|
55753
|
+
*/
|
|
55754
|
+
async callTool(params, resultSchema = CallToolResultSchema, options) {
|
|
55755
|
+
if (this.isToolTaskRequired(params.name)) throw new McpError(ErrorCode.InvalidRequest, `Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`);
|
|
55756
|
+
const result = await this.request({
|
|
55757
|
+
method: "tools/call",
|
|
55758
|
+
params
|
|
55759
|
+
}, resultSchema, options);
|
|
55760
|
+
const validator = this.getToolOutputValidator(params.name);
|
|
55761
|
+
if (validator) {
|
|
55762
|
+
if (!result.structuredContent && !result.isError) throw new McpError(ErrorCode.InvalidRequest, `Tool ${params.name} has an output schema but did not return structured content`);
|
|
55763
|
+
if (result.structuredContent) try {
|
|
55764
|
+
const validationResult = validator(result.structuredContent);
|
|
55765
|
+
if (!validationResult.valid) throw new McpError(ErrorCode.InvalidParams, `Structured content does not match the tool's output schema: ${validationResult.errorMessage}`);
|
|
55766
|
+
} catch (error) {
|
|
55767
|
+
if (error instanceof McpError) throw error;
|
|
55768
|
+
throw new McpError(ErrorCode.InvalidParams, `Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`);
|
|
55769
|
+
}
|
|
55770
|
+
}
|
|
55771
|
+
return result;
|
|
55772
|
+
}
|
|
55773
|
+
isToolTask(toolName) {
|
|
55774
|
+
if (!this._serverCapabilities?.tasks?.requests?.tools?.call) return false;
|
|
55775
|
+
return this._cachedKnownTaskTools.has(toolName);
|
|
55776
|
+
}
|
|
55777
|
+
/**
|
|
55778
|
+
* Check if a tool requires task-based execution.
|
|
55779
|
+
* Unlike isToolTask which includes 'optional' tools, this only checks for 'required'.
|
|
55780
|
+
*/
|
|
55781
|
+
isToolTaskRequired(toolName) {
|
|
55782
|
+
return this._cachedRequiredTaskTools.has(toolName);
|
|
55783
|
+
}
|
|
55784
|
+
/**
|
|
55785
|
+
* Cache validators for tool output schemas.
|
|
55786
|
+
* Called after listTools() to pre-compile validators for better performance.
|
|
55787
|
+
*/
|
|
55788
|
+
cacheToolMetadata(tools) {
|
|
55789
|
+
this._cachedToolOutputValidators.clear();
|
|
55790
|
+
this._cachedKnownTaskTools.clear();
|
|
55791
|
+
this._cachedRequiredTaskTools.clear();
|
|
55792
|
+
for (const tool of tools) {
|
|
55793
|
+
if (tool.outputSchema) {
|
|
55794
|
+
const toolValidator = this._jsonSchemaValidator.getValidator(tool.outputSchema);
|
|
55795
|
+
this._cachedToolOutputValidators.set(tool.name, toolValidator);
|
|
55796
|
+
}
|
|
55797
|
+
const taskSupport = tool.execution?.taskSupport;
|
|
55798
|
+
if (taskSupport === "required" || taskSupport === "optional") this._cachedKnownTaskTools.add(tool.name);
|
|
55799
|
+
if (taskSupport === "required") this._cachedRequiredTaskTools.add(tool.name);
|
|
55800
|
+
}
|
|
55801
|
+
}
|
|
55802
|
+
/**
|
|
55803
|
+
* Get cached validator for a tool
|
|
55804
|
+
*/
|
|
55805
|
+
getToolOutputValidator(toolName) {
|
|
55806
|
+
return this._cachedToolOutputValidators.get(toolName);
|
|
55807
|
+
}
|
|
55808
|
+
async listTools(params, options) {
|
|
55809
|
+
const result = await this.request({
|
|
55810
|
+
method: "tools/list",
|
|
55811
|
+
params
|
|
55812
|
+
}, ListToolsResultSchema, options);
|
|
55813
|
+
this.cacheToolMetadata(result.tools);
|
|
55814
|
+
return result;
|
|
55815
|
+
}
|
|
55816
|
+
/**
|
|
55817
|
+
* Set up a single list changed handler.
|
|
55818
|
+
* @internal
|
|
55819
|
+
*/
|
|
55820
|
+
_setupListChangedHandler(listType, notificationSchema, options, fetcher) {
|
|
55821
|
+
const parseResult = ListChangedOptionsBaseSchema.safeParse(options);
|
|
55822
|
+
if (!parseResult.success) throw new Error(`Invalid ${listType} listChanged options: ${parseResult.error.message}`);
|
|
55823
|
+
if (typeof options.onChanged !== "function") throw new Error(`Invalid ${listType} listChanged options: onChanged must be a function`);
|
|
55824
|
+
const { autoRefresh, debounceMs } = parseResult.data;
|
|
55825
|
+
const { onChanged } = options;
|
|
55826
|
+
const refresh = async () => {
|
|
55827
|
+
if (!autoRefresh) {
|
|
55828
|
+
onChanged(null, null);
|
|
55829
|
+
return;
|
|
55830
|
+
}
|
|
55831
|
+
try {
|
|
55832
|
+
onChanged(null, await fetcher());
|
|
55833
|
+
} catch (e) {
|
|
55834
|
+
onChanged(e instanceof Error ? e : new Error(String(e)), null);
|
|
55835
|
+
}
|
|
55836
|
+
};
|
|
55837
|
+
const handler = () => {
|
|
55838
|
+
if (debounceMs) {
|
|
55839
|
+
const existingTimer = this._listChangedDebounceTimers.get(listType);
|
|
55840
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
55841
|
+
const timer = setTimeout(refresh, debounceMs);
|
|
55842
|
+
this._listChangedDebounceTimers.set(listType, timer);
|
|
55843
|
+
} else refresh();
|
|
55844
|
+
};
|
|
55845
|
+
this.setNotificationHandler(notificationSchema, handler);
|
|
55846
|
+
}
|
|
55847
|
+
async sendRootsListChanged() {
|
|
55848
|
+
return this.notification({ method: "notifications/roots/list_changed" });
|
|
55849
|
+
}
|
|
55850
|
+
};
|
|
55851
|
+
|
|
55852
|
+
//#endregion
|
|
55853
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/transport.js
|
|
55854
|
+
/**
|
|
55855
|
+
* Normalizes HeadersInit to a plain Record<string, string> for manipulation.
|
|
55856
|
+
* Handles Headers objects, arrays of tuples, and plain objects.
|
|
55857
|
+
*/
|
|
55858
|
+
function normalizeHeaders(headers) {
|
|
55859
|
+
if (!headers) return {};
|
|
55860
|
+
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
55861
|
+
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
55862
|
+
return { ...headers };
|
|
55863
|
+
}
|
|
55864
|
+
/**
|
|
55865
|
+
* Creates a fetch function that includes base RequestInit options.
|
|
55866
|
+
* This ensures requests inherit settings like credentials, mode, headers, etc. from the base init.
|
|
55867
|
+
*
|
|
55868
|
+
* @param baseFetch - The base fetch function to wrap (defaults to global fetch)
|
|
55869
|
+
* @param baseInit - The base RequestInit to merge with each request
|
|
55870
|
+
* @returns A wrapped fetch function that merges base options with call-specific options
|
|
55871
|
+
*/
|
|
55872
|
+
function createFetchWithInit(baseFetch = fetch, baseInit) {
|
|
55873
|
+
if (!baseInit) return baseFetch;
|
|
55874
|
+
return async (url, init) => {
|
|
55875
|
+
return baseFetch(url, {
|
|
55876
|
+
...baseInit,
|
|
55877
|
+
...init,
|
|
55878
|
+
headers: init?.headers ? {
|
|
55879
|
+
...normalizeHeaders(baseInit.headers),
|
|
55880
|
+
...normalizeHeaders(init.headers)
|
|
55881
|
+
} : baseInit.headers
|
|
55882
|
+
});
|
|
55883
|
+
};
|
|
55884
|
+
}
|
|
55885
|
+
|
|
55886
|
+
//#endregion
|
|
55887
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/auth-utils.js
|
|
55888
|
+
/**
|
|
55889
|
+
* Utilities for handling OAuth resource URIs.
|
|
55890
|
+
*/
|
|
55891
|
+
/**
|
|
55892
|
+
* Converts a server URL to a resource URL by removing the fragment.
|
|
55893
|
+
* RFC 8707 section 2 states that resource URIs "MUST NOT include a fragment component".
|
|
55894
|
+
* Keeps everything else unchanged (scheme, domain, port, path, query).
|
|
55895
|
+
*/
|
|
55896
|
+
function resourceUrlFromServerUrl(url) {
|
|
55897
|
+
const resourceURL = typeof url === "string" ? new URL(url) : new URL(url.href);
|
|
55898
|
+
resourceURL.hash = "";
|
|
55899
|
+
return resourceURL;
|
|
55900
|
+
}
|
|
55901
|
+
/**
|
|
55902
|
+
* Checks if a requested resource URL matches a configured resource URL.
|
|
55903
|
+
* A requested resource matches if it has the same scheme, domain, port,
|
|
55904
|
+
* and its path starts with the configured resource's path.
|
|
55905
|
+
*
|
|
55906
|
+
* @param requestedResource The resource URL being requested
|
|
55907
|
+
* @param configuredResource The resource URL that has been configured
|
|
55908
|
+
* @returns true if the requested resource matches the configured resource, false otherwise
|
|
55909
|
+
*/
|
|
55910
|
+
function checkResourceAllowed({ requestedResource, configuredResource }) {
|
|
55911
|
+
const requested = typeof requestedResource === "string" ? new URL(requestedResource) : new URL(requestedResource.href);
|
|
55912
|
+
const configured = typeof configuredResource === "string" ? new URL(configuredResource) : new URL(configuredResource.href);
|
|
55913
|
+
if (requested.origin !== configured.origin) return false;
|
|
55914
|
+
if (requested.pathname.length < configured.pathname.length) return false;
|
|
55915
|
+
const requestedPath = requested.pathname.endsWith("/") ? requested.pathname : requested.pathname + "/";
|
|
55916
|
+
const configuredPath = configured.pathname.endsWith("/") ? configured.pathname : configured.pathname + "/";
|
|
55917
|
+
return requestedPath.startsWith(configuredPath);
|
|
55918
|
+
}
|
|
55919
|
+
|
|
55920
|
+
//#endregion
|
|
55921
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js
|
|
55922
|
+
var UnauthorizedError = class extends Error {
|
|
55923
|
+
constructor(message) {
|
|
55924
|
+
super(message ?? "Unauthorized");
|
|
55925
|
+
}
|
|
55926
|
+
};
|
|
55927
|
+
function isClientAuthMethod(method) {
|
|
55928
|
+
return [
|
|
55929
|
+
"client_secret_basic",
|
|
55930
|
+
"client_secret_post",
|
|
55931
|
+
"none"
|
|
55932
|
+
].includes(method);
|
|
55933
|
+
}
|
|
55934
|
+
const AUTHORIZATION_CODE_RESPONSE_TYPE = "code";
|
|
55935
|
+
const AUTHORIZATION_CODE_CHALLENGE_METHOD = "S256";
|
|
55936
|
+
/**
|
|
55937
|
+
* Determines the best client authentication method to use based on server support and client configuration.
|
|
55938
|
+
*
|
|
55939
|
+
* Priority order (highest to lowest):
|
|
55940
|
+
* 1. client_secret_basic (if client secret is available)
|
|
55941
|
+
* 2. client_secret_post (if client secret is available)
|
|
55942
|
+
* 3. none (for public clients)
|
|
55943
|
+
*
|
|
55944
|
+
* @param clientInformation - OAuth client information containing credentials
|
|
55945
|
+
* @param supportedMethods - Authentication methods supported by the authorization server
|
|
55946
|
+
* @returns The selected authentication method
|
|
55947
|
+
*/
|
|
55948
|
+
function selectClientAuthMethod(clientInformation, supportedMethods) {
|
|
55949
|
+
const hasClientSecret = clientInformation.client_secret !== void 0;
|
|
55950
|
+
if ("token_endpoint_auth_method" in clientInformation && clientInformation.token_endpoint_auth_method && isClientAuthMethod(clientInformation.token_endpoint_auth_method) && (supportedMethods.length === 0 || supportedMethods.includes(clientInformation.token_endpoint_auth_method))) return clientInformation.token_endpoint_auth_method;
|
|
55951
|
+
if (supportedMethods.length === 0) return hasClientSecret ? "client_secret_basic" : "none";
|
|
55952
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_basic")) return "client_secret_basic";
|
|
55953
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_post")) return "client_secret_post";
|
|
55954
|
+
if (supportedMethods.includes("none")) return "none";
|
|
55955
|
+
return hasClientSecret ? "client_secret_post" : "none";
|
|
55956
|
+
}
|
|
55957
|
+
/**
|
|
55958
|
+
* Applies client authentication to the request based on the specified method.
|
|
55959
|
+
*
|
|
55960
|
+
* Implements OAuth 2.1 client authentication methods:
|
|
55961
|
+
* - client_secret_basic: HTTP Basic authentication (RFC 6749 Section 2.3.1)
|
|
55962
|
+
* - client_secret_post: Credentials in request body (RFC 6749 Section 2.3.1)
|
|
55963
|
+
* - none: Public client authentication (RFC 6749 Section 2.1)
|
|
55964
|
+
*
|
|
55965
|
+
* @param method - The authentication method to use
|
|
55966
|
+
* @param clientInformation - OAuth client information containing credentials
|
|
55967
|
+
* @param headers - HTTP headers object to modify
|
|
55968
|
+
* @param params - URL search parameters to modify
|
|
55969
|
+
* @throws {Error} When required credentials are missing
|
|
55970
|
+
*/
|
|
55971
|
+
function applyClientAuthentication(method, clientInformation, headers, params) {
|
|
55972
|
+
const { client_id, client_secret } = clientInformation;
|
|
55973
|
+
switch (method) {
|
|
55974
|
+
case "client_secret_basic":
|
|
55975
|
+
applyBasicAuth(client_id, client_secret, headers);
|
|
55976
|
+
return;
|
|
55977
|
+
case "client_secret_post":
|
|
55978
|
+
applyPostAuth(client_id, client_secret, params);
|
|
55979
|
+
return;
|
|
55980
|
+
case "none":
|
|
55981
|
+
applyPublicAuth(client_id, params);
|
|
55982
|
+
return;
|
|
55983
|
+
default: throw new Error(`Unsupported client authentication method: ${method}`);
|
|
55984
|
+
}
|
|
55985
|
+
}
|
|
55986
|
+
/**
|
|
55987
|
+
* Applies HTTP Basic authentication (RFC 6749 Section 2.3.1)
|
|
55988
|
+
*/
|
|
55989
|
+
function applyBasicAuth(clientId, clientSecret, headers) {
|
|
55990
|
+
if (!clientSecret) throw new Error("client_secret_basic authentication requires a client_secret");
|
|
55991
|
+
const credentials = btoa(`${clientId}:${clientSecret}`);
|
|
55992
|
+
headers.set("Authorization", `Basic ${credentials}`);
|
|
55993
|
+
}
|
|
55994
|
+
/**
|
|
55995
|
+
* Applies POST body authentication (RFC 6749 Section 2.3.1)
|
|
55996
|
+
*/
|
|
55997
|
+
function applyPostAuth(clientId, clientSecret, params) {
|
|
55998
|
+
params.set("client_id", clientId);
|
|
55999
|
+
if (clientSecret) params.set("client_secret", clientSecret);
|
|
56000
|
+
}
|
|
56001
|
+
/**
|
|
56002
|
+
* Applies public client authentication (RFC 6749 Section 2.1)
|
|
56003
|
+
*/
|
|
56004
|
+
function applyPublicAuth(clientId, params) {
|
|
56005
|
+
params.set("client_id", clientId);
|
|
56006
|
+
}
|
|
56007
|
+
/**
|
|
56008
|
+
* Parses an OAuth error response from a string or Response object.
|
|
56009
|
+
*
|
|
56010
|
+
* If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
|
|
56011
|
+
* and an instance of the appropriate OAuthError subclass will be returned.
|
|
56012
|
+
* If parsing fails, it falls back to a generic ServerError that includes
|
|
56013
|
+
* the response status (if available) and original content.
|
|
56014
|
+
*
|
|
56015
|
+
* @param input - A Response object or string containing the error response
|
|
56016
|
+
* @returns A Promise that resolves to an OAuthError instance
|
|
56017
|
+
*/
|
|
56018
|
+
async function parseErrorResponse(input) {
|
|
56019
|
+
const statusCode = input instanceof Response ? input.status : void 0;
|
|
56020
|
+
const body = input instanceof Response ? await input.text() : input;
|
|
56021
|
+
try {
|
|
56022
|
+
const { error, error_description, error_uri } = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
|
56023
|
+
return new (OAUTH_ERRORS[error] || ServerError)(error_description || "", error_uri);
|
|
56024
|
+
} catch (error) {
|
|
56025
|
+
return new ServerError(`${statusCode ? `HTTP ${statusCode}: ` : ""}Invalid OAuth error response: ${error}. Raw body: ${body}`);
|
|
56026
|
+
}
|
|
56027
|
+
}
|
|
56028
|
+
/**
|
|
56029
|
+
* Orchestrates the full auth flow with a server.
|
|
56030
|
+
*
|
|
56031
|
+
* This can be used as a single entry point for all authorization functionality,
|
|
56032
|
+
* instead of linking together the other lower-level functions in this module.
|
|
56033
|
+
*/
|
|
56034
|
+
async function auth(provider, options) {
|
|
56035
|
+
try {
|
|
56036
|
+
return await authInternal(provider, options);
|
|
56037
|
+
} catch (error) {
|
|
56038
|
+
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
|
|
56039
|
+
await provider.invalidateCredentials?.("all");
|
|
56040
|
+
return await authInternal(provider, options);
|
|
56041
|
+
} else if (error instanceof InvalidGrantError) {
|
|
56042
|
+
await provider.invalidateCredentials?.("tokens");
|
|
56043
|
+
return await authInternal(provider, options);
|
|
56044
|
+
}
|
|
56045
|
+
throw error;
|
|
56046
|
+
}
|
|
56047
|
+
}
|
|
56048
|
+
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
|
56049
|
+
const cachedState = await provider.discoveryState?.();
|
|
56050
|
+
let resourceMetadata;
|
|
56051
|
+
let authorizationServerUrl;
|
|
56052
|
+
let metadata;
|
|
56053
|
+
let effectiveResourceMetadataUrl = resourceMetadataUrl;
|
|
56054
|
+
if (!effectiveResourceMetadataUrl && cachedState?.resourceMetadataUrl) effectiveResourceMetadataUrl = new URL(cachedState.resourceMetadataUrl);
|
|
56055
|
+
if (cachedState?.authorizationServerUrl) {
|
|
56056
|
+
authorizationServerUrl = cachedState.authorizationServerUrl;
|
|
56057
|
+
resourceMetadata = cachedState.resourceMetadata;
|
|
56058
|
+
metadata = cachedState.authorizationServerMetadata ?? await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn });
|
|
56059
|
+
if (!resourceMetadata) try {
|
|
56060
|
+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: effectiveResourceMetadataUrl }, fetchFn);
|
|
56061
|
+
} catch {}
|
|
56062
|
+
if (metadata !== cachedState.authorizationServerMetadata || resourceMetadata !== cachedState.resourceMetadata) await provider.saveDiscoveryState?.({
|
|
56063
|
+
authorizationServerUrl: String(authorizationServerUrl),
|
|
56064
|
+
resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
|
|
56065
|
+
resourceMetadata,
|
|
56066
|
+
authorizationServerMetadata: metadata
|
|
56067
|
+
});
|
|
56068
|
+
} else {
|
|
56069
|
+
const serverInfo = await discoverOAuthServerInfo(serverUrl, {
|
|
56070
|
+
resourceMetadataUrl: effectiveResourceMetadataUrl,
|
|
56071
|
+
fetchFn
|
|
56072
|
+
});
|
|
56073
|
+
authorizationServerUrl = serverInfo.authorizationServerUrl;
|
|
56074
|
+
metadata = serverInfo.authorizationServerMetadata;
|
|
56075
|
+
resourceMetadata = serverInfo.resourceMetadata;
|
|
56076
|
+
await provider.saveDiscoveryState?.({
|
|
56077
|
+
authorizationServerUrl: String(authorizationServerUrl),
|
|
56078
|
+
resourceMetadataUrl: effectiveResourceMetadataUrl?.toString(),
|
|
56079
|
+
resourceMetadata,
|
|
56080
|
+
authorizationServerMetadata: metadata
|
|
56081
|
+
});
|
|
56082
|
+
}
|
|
56083
|
+
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
|
56084
|
+
const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(" ") || provider.clientMetadata.scope;
|
|
56085
|
+
let clientInformation = await Promise.resolve(provider.clientInformation());
|
|
56086
|
+
if (!clientInformation) {
|
|
56087
|
+
if (authorizationCode !== void 0) throw new Error("Existing OAuth client information is required when exchanging an authorization code");
|
|
56088
|
+
const supportsUrlBasedClientId = metadata?.client_id_metadata_document_supported === true;
|
|
56089
|
+
const clientMetadataUrl = provider.clientMetadataUrl;
|
|
56090
|
+
if (clientMetadataUrl && !isHttpsUrl(clientMetadataUrl)) throw new InvalidClientMetadataError(`clientMetadataUrl must be a valid HTTPS URL with a non-root pathname, got: ${clientMetadataUrl}`);
|
|
56091
|
+
if (supportsUrlBasedClientId && clientMetadataUrl) {
|
|
56092
|
+
clientInformation = { client_id: clientMetadataUrl };
|
|
56093
|
+
await provider.saveClientInformation?.(clientInformation);
|
|
56094
|
+
} else {
|
|
56095
|
+
if (!provider.saveClientInformation) throw new Error("OAuth client information must be saveable for dynamic registration");
|
|
56096
|
+
const fullInformation = await registerClient(authorizationServerUrl, {
|
|
56097
|
+
metadata,
|
|
56098
|
+
clientMetadata: provider.clientMetadata,
|
|
56099
|
+
scope: resolvedScope,
|
|
56100
|
+
fetchFn
|
|
56101
|
+
});
|
|
56102
|
+
await provider.saveClientInformation(fullInformation);
|
|
56103
|
+
clientInformation = fullInformation;
|
|
56104
|
+
}
|
|
56105
|
+
}
|
|
56106
|
+
const nonInteractiveFlow = !provider.redirectUrl;
|
|
56107
|
+
if (authorizationCode !== void 0 || nonInteractiveFlow) {
|
|
56108
|
+
const tokens = await fetchToken(provider, authorizationServerUrl, {
|
|
56109
|
+
metadata,
|
|
56110
|
+
resource,
|
|
56111
|
+
authorizationCode,
|
|
56112
|
+
fetchFn
|
|
56113
|
+
});
|
|
56114
|
+
await provider.saveTokens(tokens);
|
|
56115
|
+
return "AUTHORIZED";
|
|
56116
|
+
}
|
|
56117
|
+
const tokens = await provider.tokens();
|
|
56118
|
+
if (tokens?.refresh_token) try {
|
|
56119
|
+
const newTokens = await refreshAuthorization(authorizationServerUrl, {
|
|
56120
|
+
metadata,
|
|
56121
|
+
clientInformation,
|
|
56122
|
+
refreshToken: tokens.refresh_token,
|
|
56123
|
+
resource,
|
|
56124
|
+
addClientAuthentication: provider.addClientAuthentication,
|
|
56125
|
+
fetchFn
|
|
56126
|
+
});
|
|
56127
|
+
await provider.saveTokens(newTokens);
|
|
56128
|
+
return "AUTHORIZED";
|
|
56129
|
+
} catch (error) {
|
|
56130
|
+
if (!(error instanceof OAuthError) || error instanceof ServerError) {} else throw error;
|
|
56131
|
+
}
|
|
56132
|
+
const state = provider.state ? await provider.state() : void 0;
|
|
56133
|
+
const { authorizationUrl, codeVerifier } = await startAuthorization(authorizationServerUrl, {
|
|
56134
|
+
metadata,
|
|
56135
|
+
clientInformation,
|
|
56136
|
+
state,
|
|
56137
|
+
redirectUrl: provider.redirectUrl,
|
|
56138
|
+
scope: resolvedScope,
|
|
56139
|
+
resource
|
|
56140
|
+
});
|
|
56141
|
+
await provider.saveCodeVerifier(codeVerifier);
|
|
56142
|
+
await provider.redirectToAuthorization(authorizationUrl);
|
|
56143
|
+
return "REDIRECT";
|
|
56144
|
+
}
|
|
56145
|
+
/**
|
|
56146
|
+
* SEP-991: URL-based Client IDs
|
|
56147
|
+
* Validate that the client_id is a valid URL with https scheme
|
|
56148
|
+
*/
|
|
56149
|
+
function isHttpsUrl(value) {
|
|
56150
|
+
if (!value) return false;
|
|
56151
|
+
try {
|
|
56152
|
+
const url = new URL(value);
|
|
56153
|
+
return url.protocol === "https:" && url.pathname !== "/";
|
|
56154
|
+
} catch {
|
|
56155
|
+
return false;
|
|
56156
|
+
}
|
|
56157
|
+
}
|
|
56158
|
+
async function selectResourceURL(serverUrl, provider, resourceMetadata) {
|
|
56159
|
+
const defaultResource = resourceUrlFromServerUrl(serverUrl);
|
|
56160
|
+
if (provider.validateResourceURL) return await provider.validateResourceURL(defaultResource, resourceMetadata?.resource);
|
|
56161
|
+
if (!resourceMetadata) return;
|
|
56162
|
+
if (!checkResourceAllowed({
|
|
56163
|
+
requestedResource: defaultResource,
|
|
56164
|
+
configuredResource: resourceMetadata.resource
|
|
56165
|
+
})) throw new Error(`Protected resource ${resourceMetadata.resource} does not match expected ${defaultResource} (or origin)`);
|
|
56166
|
+
return new URL(resourceMetadata.resource);
|
|
56167
|
+
}
|
|
56168
|
+
/**
|
|
56169
|
+
* Extract resource_metadata, scope, and error from WWW-Authenticate header.
|
|
56170
|
+
*/
|
|
56171
|
+
function extractWWWAuthenticateParams(res) {
|
|
56172
|
+
const authenticateHeader = res.headers.get("WWW-Authenticate");
|
|
56173
|
+
if (!authenticateHeader) return {};
|
|
56174
|
+
const [type, scheme] = authenticateHeader.split(" ");
|
|
56175
|
+
if (type.toLowerCase() !== "bearer" || !scheme) return {};
|
|
56176
|
+
const resourceMetadataMatch = extractFieldFromWwwAuth(res, "resource_metadata") || void 0;
|
|
56177
|
+
let resourceMetadataUrl;
|
|
56178
|
+
if (resourceMetadataMatch) try {
|
|
56179
|
+
resourceMetadataUrl = new URL(resourceMetadataMatch);
|
|
56180
|
+
} catch {}
|
|
56181
|
+
const scope = extractFieldFromWwwAuth(res, "scope") || void 0;
|
|
56182
|
+
const error = extractFieldFromWwwAuth(res, "error") || void 0;
|
|
56183
|
+
return {
|
|
56184
|
+
resourceMetadataUrl,
|
|
56185
|
+
scope,
|
|
56186
|
+
error
|
|
56187
|
+
};
|
|
56188
|
+
}
|
|
56189
|
+
/**
|
|
56190
|
+
* Extracts a specific field's value from the WWW-Authenticate header string.
|
|
56191
|
+
*
|
|
56192
|
+
* @param response The HTTP response object containing the headers.
|
|
56193
|
+
* @param fieldName The name of the field to extract (e.g., "realm", "nonce").
|
|
56194
|
+
* @returns The field value
|
|
56195
|
+
*/
|
|
56196
|
+
function extractFieldFromWwwAuth(response, fieldName) {
|
|
56197
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
56198
|
+
if (!wwwAuthHeader) return null;
|
|
56199
|
+
const pattern = new RegExp(`${fieldName}=(?:"([^"]+)"|([^\\s,]+))`);
|
|
56200
|
+
const match = wwwAuthHeader.match(pattern);
|
|
56201
|
+
if (match) return match[1] || match[2];
|
|
56202
|
+
return null;
|
|
56203
|
+
}
|
|
56204
|
+
/**
|
|
56205
|
+
* Looks up RFC 9728 OAuth 2.0 Protected Resource Metadata.
|
|
56206
|
+
*
|
|
56207
|
+
* If the server returns a 404 for the well-known endpoint, this function will
|
|
56208
|
+
* return `undefined`. Any other errors will be thrown as exceptions.
|
|
56209
|
+
*/
|
|
56210
|
+
async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
|
|
56211
|
+
const response = await discoverMetadataWithFallback(serverUrl, "oauth-protected-resource", fetchFn, {
|
|
56212
|
+
protocolVersion: opts?.protocolVersion,
|
|
56213
|
+
metadataUrl: opts?.resourceMetadataUrl
|
|
56214
|
+
});
|
|
56215
|
+
if (!response || response.status === 404) {
|
|
56216
|
+
await response?.body?.cancel();
|
|
56217
|
+
throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
|
|
56218
|
+
}
|
|
56219
|
+
if (!response.ok) {
|
|
56220
|
+
await response.body?.cancel();
|
|
56221
|
+
throw new Error(`HTTP ${response.status} trying to load well-known OAuth protected resource metadata.`);
|
|
56222
|
+
}
|
|
56223
|
+
return OAuthProtectedResourceMetadataSchema.parse(await response.json());
|
|
56224
|
+
}
|
|
56225
|
+
/**
|
|
56226
|
+
* Helper function to handle fetch with CORS retry logic
|
|
56227
|
+
*/
|
|
56228
|
+
async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
|
|
56229
|
+
try {
|
|
56230
|
+
return await fetchFn(url, { headers });
|
|
56231
|
+
} catch (error) {
|
|
56232
|
+
if (error instanceof TypeError) if (headers) return fetchWithCorsRetry(url, void 0, fetchFn);
|
|
56233
|
+
else return;
|
|
56234
|
+
throw error;
|
|
56235
|
+
}
|
|
56236
|
+
}
|
|
56237
|
+
/**
|
|
56238
|
+
* Constructs the well-known path for auth-related metadata discovery
|
|
56239
|
+
*/
|
|
56240
|
+
function buildWellKnownPath(wellKnownPrefix, pathname = "", options = {}) {
|
|
56241
|
+
if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
56242
|
+
return options.prependPathname ? `${pathname}/.well-known/${wellKnownPrefix}` : `/.well-known/${wellKnownPrefix}${pathname}`;
|
|
56243
|
+
}
|
|
56244
|
+
/**
|
|
56245
|
+
* Tries to discover OAuth metadata at a specific URL
|
|
56246
|
+
*/
|
|
56247
|
+
async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
|
|
56248
|
+
return await fetchWithCorsRetry(url, { "MCP-Protocol-Version": protocolVersion }, fetchFn);
|
|
56249
|
+
}
|
|
56250
|
+
/**
|
|
56251
|
+
* Determines if fallback to root discovery should be attempted
|
|
56252
|
+
*/
|
|
56253
|
+
function shouldAttemptFallback(response, pathname) {
|
|
56254
|
+
return !response || response.status >= 400 && response.status < 500 && pathname !== "/";
|
|
56255
|
+
}
|
|
56256
|
+
/**
|
|
56257
|
+
* Generic function for discovering OAuth metadata with fallback support
|
|
56258
|
+
*/
|
|
56259
|
+
async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
|
|
56260
|
+
const issuer = new URL(serverUrl);
|
|
56261
|
+
const protocolVersion = opts?.protocolVersion ?? "2025-11-25";
|
|
56262
|
+
let url;
|
|
56263
|
+
if (opts?.metadataUrl) url = new URL(opts.metadataUrl);
|
|
56264
|
+
else {
|
|
56265
|
+
const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
|
|
56266
|
+
url = new URL(wellKnownPath, opts?.metadataServerUrl ?? issuer);
|
|
56267
|
+
url.search = issuer.search;
|
|
56268
|
+
}
|
|
56269
|
+
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
|
|
56270
|
+
if (!opts?.metadataUrl && shouldAttemptFallback(response, issuer.pathname)) response = await tryMetadataDiscovery(new URL(`/.well-known/${wellKnownType}`, issuer), protocolVersion, fetchFn);
|
|
56271
|
+
return response;
|
|
56272
|
+
}
|
|
56273
|
+
/**
|
|
56274
|
+
* Builds a list of discovery URLs to try for authorization server metadata.
|
|
56275
|
+
* URLs are returned in priority order:
|
|
56276
|
+
* 1. OAuth metadata at the given URL
|
|
56277
|
+
* 2. OIDC metadata endpoints at the given URL
|
|
56278
|
+
*/
|
|
56279
|
+
function buildDiscoveryUrls(authorizationServerUrl) {
|
|
56280
|
+
const url = typeof authorizationServerUrl === "string" ? new URL(authorizationServerUrl) : authorizationServerUrl;
|
|
56281
|
+
const hasPath = url.pathname !== "/";
|
|
56282
|
+
const urlsToTry = [];
|
|
56283
|
+
if (!hasPath) {
|
|
56284
|
+
urlsToTry.push({
|
|
56285
|
+
url: new URL("/.well-known/oauth-authorization-server", url.origin),
|
|
56286
|
+
type: "oauth"
|
|
56287
|
+
});
|
|
56288
|
+
urlsToTry.push({
|
|
56289
|
+
url: new URL(`/.well-known/openid-configuration`, url.origin),
|
|
56290
|
+
type: "oidc"
|
|
56291
|
+
});
|
|
56292
|
+
return urlsToTry;
|
|
56293
|
+
}
|
|
56294
|
+
let pathname = url.pathname;
|
|
56295
|
+
if (pathname.endsWith("/")) pathname = pathname.slice(0, -1);
|
|
56296
|
+
urlsToTry.push({
|
|
56297
|
+
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
|
|
56298
|
+
type: "oauth"
|
|
56299
|
+
});
|
|
56300
|
+
urlsToTry.push({
|
|
56301
|
+
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
|
|
56302
|
+
type: "oidc"
|
|
56303
|
+
});
|
|
56304
|
+
urlsToTry.push({
|
|
56305
|
+
url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
|
|
56306
|
+
type: "oidc"
|
|
56307
|
+
});
|
|
56308
|
+
return urlsToTry;
|
|
56309
|
+
}
|
|
56310
|
+
/**
|
|
56311
|
+
* Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
|
|
56312
|
+
* and OpenID Connect Discovery 1.0 specifications.
|
|
56313
|
+
*
|
|
56314
|
+
* This function implements a fallback strategy for authorization server discovery:
|
|
56315
|
+
* 1. Attempts RFC 8414 OAuth metadata discovery first
|
|
56316
|
+
* 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
|
|
56317
|
+
*
|
|
56318
|
+
* @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
|
|
56319
|
+
* protected resource metadata, or the MCP server's URL if the
|
|
56320
|
+
* metadata was not found.
|
|
56321
|
+
* @param options - Configuration options
|
|
56322
|
+
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
|
|
56323
|
+
* @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
|
|
56324
|
+
* @returns Promise resolving to authorization server metadata, or undefined if discovery fails
|
|
56325
|
+
*/
|
|
56326
|
+
async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION } = {}) {
|
|
56327
|
+
const headers = {
|
|
56328
|
+
"MCP-Protocol-Version": protocolVersion,
|
|
56329
|
+
Accept: "application/json"
|
|
56330
|
+
};
|
|
56331
|
+
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
56332
|
+
for (const { url: endpointUrl, type } of urlsToTry) {
|
|
56333
|
+
const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
|
|
56334
|
+
if (!response)
|
|
56335
|
+
/**
|
|
56336
|
+
* CORS error occurred - don't throw as the endpoint may not allow CORS,
|
|
56337
|
+
* continue trying other possible endpoints
|
|
56338
|
+
*/
|
|
56339
|
+
continue;
|
|
56340
|
+
if (!response.ok) {
|
|
56341
|
+
await response.body?.cancel();
|
|
56342
|
+
if (response.status >= 400 && response.status < 500) continue;
|
|
56343
|
+
throw new Error(`HTTP ${response.status} trying to load ${type === "oauth" ? "OAuth" : "OpenID provider"} metadata from ${endpointUrl}`);
|
|
56344
|
+
}
|
|
56345
|
+
if (type === "oauth") return OAuthMetadataSchema.parse(await response.json());
|
|
56346
|
+
else return OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
|
|
56347
|
+
}
|
|
56348
|
+
}
|
|
56349
|
+
/**
|
|
56350
|
+
* Discovers the authorization server for an MCP server following
|
|
56351
|
+
* {@link https://datatracker.ietf.org/doc/html/rfc9728 | RFC 9728} (OAuth 2.0 Protected
|
|
56352
|
+
* Resource Metadata), with fallback to treating the server URL as the
|
|
56353
|
+
* authorization server.
|
|
56354
|
+
*
|
|
56355
|
+
* This function combines two discovery steps into one call:
|
|
56356
|
+
* 1. Probes `/.well-known/oauth-protected-resource` on the MCP server to find the
|
|
56357
|
+
* authorization server URL (RFC 9728).
|
|
56358
|
+
* 2. Fetches authorization server metadata from that URL (RFC 8414 / OpenID Connect Discovery).
|
|
56359
|
+
*
|
|
56360
|
+
* Use this when you need the authorization server metadata for operations outside the
|
|
56361
|
+
* {@linkcode auth} orchestrator, such as token refresh or token revocation.
|
|
56362
|
+
*
|
|
56363
|
+
* @param serverUrl - The MCP resource server URL
|
|
56364
|
+
* @param opts - Optional configuration
|
|
56365
|
+
* @param opts.resourceMetadataUrl - Override URL for the protected resource metadata endpoint
|
|
56366
|
+
* @param opts.fetchFn - Custom fetch function for HTTP requests
|
|
56367
|
+
* @returns Authorization server URL, metadata, and resource metadata (if available)
|
|
56368
|
+
*/
|
|
56369
|
+
async function discoverOAuthServerInfo(serverUrl, opts) {
|
|
56370
|
+
let resourceMetadata;
|
|
56371
|
+
let authorizationServerUrl;
|
|
56372
|
+
try {
|
|
56373
|
+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl: opts?.resourceMetadataUrl }, opts?.fetchFn);
|
|
56374
|
+
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
|
56375
|
+
} catch {}
|
|
56376
|
+
if (!authorizationServerUrl) authorizationServerUrl = String(new URL("/", serverUrl));
|
|
56377
|
+
const authorizationServerMetadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn: opts?.fetchFn });
|
|
56378
|
+
return {
|
|
56379
|
+
authorizationServerUrl,
|
|
56380
|
+
authorizationServerMetadata,
|
|
56381
|
+
resourceMetadata
|
|
56382
|
+
};
|
|
56383
|
+
}
|
|
56384
|
+
/**
|
|
56385
|
+
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
|
|
56386
|
+
*/
|
|
56387
|
+
async function startAuthorization(authorizationServerUrl, { metadata, clientInformation, redirectUrl, scope, state, resource }) {
|
|
56388
|
+
let authorizationUrl;
|
|
56389
|
+
if (metadata) {
|
|
56390
|
+
authorizationUrl = new URL(metadata.authorization_endpoint);
|
|
56391
|
+
if (!metadata.response_types_supported.includes(AUTHORIZATION_CODE_RESPONSE_TYPE)) throw new Error(`Incompatible auth server: does not support response type ${AUTHORIZATION_CODE_RESPONSE_TYPE}`);
|
|
56392
|
+
if (metadata.code_challenge_methods_supported && !metadata.code_challenge_methods_supported.includes(AUTHORIZATION_CODE_CHALLENGE_METHOD)) throw new Error(`Incompatible auth server: does not support code challenge method ${AUTHORIZATION_CODE_CHALLENGE_METHOD}`);
|
|
56393
|
+
} else authorizationUrl = new URL("/authorize", authorizationServerUrl);
|
|
56394
|
+
const challenge = await pkceChallenge();
|
|
56395
|
+
const codeVerifier = challenge.code_verifier;
|
|
56396
|
+
const codeChallenge = challenge.code_challenge;
|
|
56397
|
+
authorizationUrl.searchParams.set("response_type", AUTHORIZATION_CODE_RESPONSE_TYPE);
|
|
56398
|
+
authorizationUrl.searchParams.set("client_id", clientInformation.client_id);
|
|
56399
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
56400
|
+
authorizationUrl.searchParams.set("code_challenge_method", AUTHORIZATION_CODE_CHALLENGE_METHOD);
|
|
56401
|
+
authorizationUrl.searchParams.set("redirect_uri", String(redirectUrl));
|
|
56402
|
+
if (state) authorizationUrl.searchParams.set("state", state);
|
|
56403
|
+
if (scope) authorizationUrl.searchParams.set("scope", scope);
|
|
56404
|
+
if (scope?.includes("offline_access")) authorizationUrl.searchParams.append("prompt", "consent");
|
|
56405
|
+
if (resource) authorizationUrl.searchParams.set("resource", resource.href);
|
|
56406
|
+
return {
|
|
56407
|
+
authorizationUrl,
|
|
56408
|
+
codeVerifier
|
|
56409
|
+
};
|
|
56410
|
+
}
|
|
56411
|
+
/**
|
|
56412
|
+
* Prepares token request parameters for an authorization code exchange.
|
|
56413
|
+
*
|
|
56414
|
+
* This is the default implementation used by fetchToken when the provider
|
|
56415
|
+
* doesn't implement prepareTokenRequest.
|
|
56416
|
+
*
|
|
56417
|
+
* @param authorizationCode - The authorization code received from the authorization endpoint
|
|
56418
|
+
* @param codeVerifier - The PKCE code verifier
|
|
56419
|
+
* @param redirectUri - The redirect URI used in the authorization request
|
|
56420
|
+
* @returns URLSearchParams for the authorization_code grant
|
|
56421
|
+
*/
|
|
56422
|
+
function prepareAuthorizationCodeRequest(authorizationCode, codeVerifier, redirectUri) {
|
|
56423
|
+
return new URLSearchParams({
|
|
56424
|
+
grant_type: "authorization_code",
|
|
56425
|
+
code: authorizationCode,
|
|
56426
|
+
code_verifier: codeVerifier,
|
|
56427
|
+
redirect_uri: String(redirectUri)
|
|
56428
|
+
});
|
|
56429
|
+
}
|
|
56430
|
+
/**
|
|
56431
|
+
* Internal helper to execute a token request with the given parameters.
|
|
56432
|
+
* Used by exchangeAuthorization, refreshAuthorization, and fetchToken.
|
|
56433
|
+
*/
|
|
56434
|
+
async function executeTokenRequest(authorizationServerUrl, { metadata, tokenRequestParams, clientInformation, addClientAuthentication, resource, fetchFn }) {
|
|
56435
|
+
const tokenUrl = metadata?.token_endpoint ? new URL(metadata.token_endpoint) : new URL("/token", authorizationServerUrl);
|
|
56436
|
+
const headers = new Headers({
|
|
56437
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
56438
|
+
Accept: "application/json"
|
|
56439
|
+
});
|
|
56440
|
+
if (resource) tokenRequestParams.set("resource", resource.href);
|
|
56441
|
+
if (addClientAuthentication) await addClientAuthentication(headers, tokenRequestParams, tokenUrl, metadata);
|
|
56442
|
+
else if (clientInformation) applyClientAuthentication(selectClientAuthMethod(clientInformation, metadata?.token_endpoint_auth_methods_supported ?? []), clientInformation, headers, tokenRequestParams);
|
|
56443
|
+
const response = await (fetchFn ?? fetch)(tokenUrl, {
|
|
56444
|
+
method: "POST",
|
|
56445
|
+
headers,
|
|
56446
|
+
body: tokenRequestParams
|
|
56447
|
+
});
|
|
56448
|
+
if (!response.ok) throw await parseErrorResponse(response);
|
|
56449
|
+
return OAuthTokensSchema.parse(await response.json());
|
|
56450
|
+
}
|
|
56451
|
+
/**
|
|
56452
|
+
* Exchange a refresh token for an updated access token.
|
|
56453
|
+
*
|
|
56454
|
+
* Supports multiple client authentication methods as specified in OAuth 2.1:
|
|
56455
|
+
* - Automatically selects the best authentication method based on server support
|
|
56456
|
+
* - Preserves the original refresh token if a new one is not returned
|
|
56457
|
+
*
|
|
56458
|
+
* @param authorizationServerUrl - The authorization server's base URL
|
|
56459
|
+
* @param options - Configuration object containing client info, refresh token, etc.
|
|
56460
|
+
* @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
|
|
56461
|
+
* @throws {Error} When token refresh fails or authentication is invalid
|
|
56462
|
+
*/
|
|
56463
|
+
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
|
56464
|
+
return {
|
|
56465
|
+
refresh_token: refreshToken,
|
|
56466
|
+
...await executeTokenRequest(authorizationServerUrl, {
|
|
56467
|
+
metadata,
|
|
56468
|
+
tokenRequestParams: new URLSearchParams({
|
|
56469
|
+
grant_type: "refresh_token",
|
|
56470
|
+
refresh_token: refreshToken
|
|
56471
|
+
}),
|
|
56472
|
+
clientInformation,
|
|
56473
|
+
addClientAuthentication,
|
|
56474
|
+
resource,
|
|
56475
|
+
fetchFn
|
|
56476
|
+
})
|
|
56477
|
+
};
|
|
56478
|
+
}
|
|
56479
|
+
/**
|
|
56480
|
+
* Unified token fetching that works with any grant type via provider.prepareTokenRequest().
|
|
56481
|
+
*
|
|
56482
|
+
* This function provides a single entry point for obtaining tokens regardless of the
|
|
56483
|
+
* OAuth grant type. The provider's prepareTokenRequest() method determines which grant
|
|
56484
|
+
* to use and supplies the grant-specific parameters.
|
|
56485
|
+
*
|
|
56486
|
+
* @param provider - OAuth client provider that implements prepareTokenRequest()
|
|
56487
|
+
* @param authorizationServerUrl - The authorization server's base URL
|
|
56488
|
+
* @param options - Configuration for the token request
|
|
56489
|
+
* @returns Promise resolving to OAuth tokens
|
|
56490
|
+
* @throws {Error} When provider doesn't implement prepareTokenRequest or token fetch fails
|
|
56491
|
+
*
|
|
56492
|
+
* @example
|
|
56493
|
+
* // Provider for client_credentials:
|
|
56494
|
+
* class MyProvider implements OAuthClientProvider {
|
|
56495
|
+
* prepareTokenRequest(scope) {
|
|
56496
|
+
* const params = new URLSearchParams({ grant_type: 'client_credentials' });
|
|
56497
|
+
* if (scope) params.set('scope', scope);
|
|
56498
|
+
* return params;
|
|
56499
|
+
* }
|
|
56500
|
+
* // ... other methods
|
|
56501
|
+
* }
|
|
56502
|
+
*
|
|
56503
|
+
* const tokens = await fetchToken(provider, authServerUrl, { metadata });
|
|
56504
|
+
*/
|
|
56505
|
+
async function fetchToken(provider, authorizationServerUrl, { metadata, resource, authorizationCode, fetchFn } = {}) {
|
|
56506
|
+
const scope = provider.clientMetadata.scope;
|
|
56507
|
+
let tokenRequestParams;
|
|
56508
|
+
if (provider.prepareTokenRequest) tokenRequestParams = await provider.prepareTokenRequest(scope);
|
|
56509
|
+
if (!tokenRequestParams) {
|
|
56510
|
+
if (!authorizationCode) throw new Error("Either provider.prepareTokenRequest() or authorizationCode is required");
|
|
56511
|
+
if (!provider.redirectUrl) throw new Error("redirectUrl is required for authorization_code flow");
|
|
56512
|
+
tokenRequestParams = prepareAuthorizationCodeRequest(authorizationCode, await provider.codeVerifier(), provider.redirectUrl);
|
|
56513
|
+
}
|
|
56514
|
+
const clientInformation = await provider.clientInformation();
|
|
56515
|
+
return executeTokenRequest(authorizationServerUrl, {
|
|
56516
|
+
metadata,
|
|
56517
|
+
tokenRequestParams,
|
|
56518
|
+
clientInformation: clientInformation ?? void 0,
|
|
56519
|
+
addClientAuthentication: provider.addClientAuthentication,
|
|
56520
|
+
resource,
|
|
56521
|
+
fetchFn
|
|
56522
|
+
});
|
|
56523
|
+
}
|
|
56524
|
+
/**
|
|
56525
|
+
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
|
|
56526
|
+
*
|
|
56527
|
+
* If `scope` is provided, it overrides `clientMetadata.scope` in the registration
|
|
56528
|
+
* request body. This allows callers to apply the Scope Selection Strategy (SEP-835)
|
|
56529
|
+
* consistently across both DCR and the subsequent authorization request.
|
|
56530
|
+
*/
|
|
56531
|
+
async function registerClient(authorizationServerUrl, { metadata, clientMetadata, scope, fetchFn }) {
|
|
56532
|
+
let registrationUrl;
|
|
56533
|
+
if (metadata) {
|
|
56534
|
+
if (!metadata.registration_endpoint) throw new Error("Incompatible auth server: does not support dynamic client registration");
|
|
56535
|
+
registrationUrl = new URL(metadata.registration_endpoint);
|
|
56536
|
+
} else registrationUrl = new URL("/register", authorizationServerUrl);
|
|
56537
|
+
const response = await (fetchFn ?? fetch)(registrationUrl, {
|
|
56538
|
+
method: "POST",
|
|
56539
|
+
headers: { "Content-Type": "application/json" },
|
|
56540
|
+
body: JSON.stringify({
|
|
56541
|
+
...clientMetadata,
|
|
56542
|
+
...scope !== void 0 ? { scope } : {}
|
|
56543
|
+
})
|
|
56544
|
+
});
|
|
56545
|
+
if (!response.ok) throw await parseErrorResponse(response);
|
|
56546
|
+
return OAuthClientInformationFullSchema.parse(await response.json());
|
|
56547
|
+
}
|
|
56548
|
+
|
|
56549
|
+
//#endregion
|
|
56550
|
+
//#region ../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/index.js
|
|
56551
|
+
var ParseError = class extends Error {
|
|
56552
|
+
constructor(message, options) {
|
|
56553
|
+
super(message), this.name = "ParseError", this.type = options.type, this.field = options.field, this.value = options.value, this.line = options.line;
|
|
56554
|
+
}
|
|
56555
|
+
};
|
|
56556
|
+
const LF = 10, CR = 13, SPACE = 32;
|
|
56557
|
+
function noop$2(_arg) {}
|
|
56558
|
+
function createParser(config) {
|
|
56559
|
+
if (typeof config == "function") throw new TypeError("`config` must be an object, got a function instead. Did you mean `createParser({onEvent: fn})`?");
|
|
56560
|
+
const { onEvent = noop$2, onError = noop$2, onRetry = noop$2, onComment, maxBufferSize } = config, pendingFragments = [];
|
|
56561
|
+
let pendingFragmentsLength = 0, isFirstChunk = !0, id, data = "", dataLines = 0, eventType, terminated = !1;
|
|
56562
|
+
function feed(chunk) {
|
|
56563
|
+
if (terminated) throw new Error("Cannot feed parser: it was terminated after exceeding the configured max buffer size. Call `reset()` to resume parsing.");
|
|
56564
|
+
if (isFirstChunk && (isFirstChunk = !1, chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3))), pendingFragments.length === 0) {
|
|
56565
|
+
const trailing2 = processLines(chunk);
|
|
56566
|
+
trailing2 !== "" && (pendingFragments.push(trailing2), pendingFragmentsLength = trailing2.length), checkBufferSize();
|
|
56567
|
+
return;
|
|
56568
|
+
}
|
|
56569
|
+
if (chunk.indexOf(`
|
|
56570
|
+
`) === -1 && chunk.indexOf("\r") === -1) {
|
|
56571
|
+
pendingFragments.push(chunk), pendingFragmentsLength += chunk.length, checkBufferSize();
|
|
56572
|
+
return;
|
|
56573
|
+
}
|
|
56574
|
+
pendingFragments.push(chunk);
|
|
56575
|
+
const input = pendingFragments.join("");
|
|
56576
|
+
pendingFragments.length = 0, pendingFragmentsLength = 0;
|
|
56577
|
+
const trailing = processLines(input);
|
|
56578
|
+
trailing !== "" && (pendingFragments.push(trailing), pendingFragmentsLength = trailing.length), checkBufferSize();
|
|
56579
|
+
}
|
|
56580
|
+
function checkBufferSize() {
|
|
56581
|
+
maxBufferSize !== void 0 && (pendingFragmentsLength + data.length <= maxBufferSize || (terminated = !0, pendingFragments.length = 0, pendingFragmentsLength = 0, id = void 0, data = "", dataLines = 0, eventType = void 0, onError(new ParseError(`Buffered data exceeded max buffer size of ${maxBufferSize} characters`, { type: "max-buffer-size-exceeded" }))));
|
|
56582
|
+
}
|
|
56583
|
+
function processLines(chunk) {
|
|
56584
|
+
let searchIndex = 0;
|
|
56585
|
+
if (chunk.indexOf("\r") === -1) {
|
|
56586
|
+
let lfIndex = chunk.indexOf(`
|
|
56587
|
+
`, searchIndex);
|
|
56588
|
+
for (; lfIndex !== -1;) {
|
|
56589
|
+
if (searchIndex === lfIndex) {
|
|
56590
|
+
dataLines > 0 && onEvent({
|
|
56591
|
+
id,
|
|
56592
|
+
event: eventType,
|
|
56593
|
+
data
|
|
56594
|
+
}), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
|
|
56595
|
+
`, searchIndex);
|
|
56596
|
+
continue;
|
|
56597
|
+
}
|
|
56598
|
+
const firstCharCode = chunk.charCodeAt(searchIndex);
|
|
56599
|
+
if (isDataPrefix(chunk, searchIndex, firstCharCode)) {
|
|
56600
|
+
const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex);
|
|
56601
|
+
if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {
|
|
56602
|
+
onEvent({
|
|
56603
|
+
id,
|
|
56604
|
+
event: eventType,
|
|
56605
|
+
data: value
|
|
56606
|
+
}), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(`
|
|
56607
|
+
`, searchIndex);
|
|
56608
|
+
continue;
|
|
56609
|
+
}
|
|
56610
|
+
data = dataLines === 0 ? value : `${data}
|
|
56611
|
+
${value}`, dataLines++;
|
|
56612
|
+
} else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice(chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, lfIndex) || void 0 : parseLine(chunk, searchIndex, lfIndex);
|
|
56613
|
+
searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(`
|
|
56614
|
+
`, searchIndex);
|
|
56615
|
+
}
|
|
56616
|
+
return chunk.slice(searchIndex);
|
|
56617
|
+
}
|
|
56618
|
+
for (; searchIndex < chunk.length;) {
|
|
56619
|
+
const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(`
|
|
56620
|
+
`, searchIndex);
|
|
56621
|
+
let lineEnd = -1;
|
|
56622
|
+
if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) break;
|
|
56623
|
+
parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++;
|
|
56624
|
+
}
|
|
56625
|
+
return chunk.slice(searchIndex);
|
|
56626
|
+
}
|
|
56627
|
+
function parseLine(chunk, start, end) {
|
|
56628
|
+
if (start === end) {
|
|
56629
|
+
dispatchEvent();
|
|
56630
|
+
return;
|
|
56631
|
+
}
|
|
56632
|
+
const firstCharCode = chunk.charCodeAt(start);
|
|
56633
|
+
if (isDataPrefix(chunk, start, firstCharCode)) {
|
|
56634
|
+
const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end);
|
|
56635
|
+
data = dataLines === 0 ? value2 : `${data}
|
|
56636
|
+
${value2}`, dataLines++;
|
|
56637
|
+
return;
|
|
56638
|
+
}
|
|
56639
|
+
if (isEventPrefix(chunk, start, firstCharCode)) {
|
|
56640
|
+
eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0;
|
|
56641
|
+
return;
|
|
56642
|
+
}
|
|
56643
|
+
if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) {
|
|
56644
|
+
const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end);
|
|
56645
|
+
id = value2.includes("\0") ? void 0 : value2;
|
|
56646
|
+
return;
|
|
56647
|
+
}
|
|
56648
|
+
if (firstCharCode === 58) {
|
|
56649
|
+
if (onComment) onComment(chunk.slice(start, end).slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1));
|
|
56650
|
+
return;
|
|
56651
|
+
}
|
|
56652
|
+
const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":");
|
|
56653
|
+
if (fieldSeparatorIndex === -1) {
|
|
56654
|
+
processField(line, "", line);
|
|
56655
|
+
return;
|
|
56656
|
+
}
|
|
56657
|
+
const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1;
|
|
56658
|
+
processField(field, line.slice(fieldSeparatorIndex + offset), line);
|
|
56659
|
+
}
|
|
56660
|
+
function processField(field, value, line) {
|
|
56661
|
+
switch (field) {
|
|
56662
|
+
case "event":
|
|
56663
|
+
eventType = value || void 0;
|
|
56664
|
+
break;
|
|
56665
|
+
case "data":
|
|
56666
|
+
data = dataLines === 0 ? value : `${data}
|
|
56667
|
+
${value}`, dataLines++;
|
|
56668
|
+
break;
|
|
56669
|
+
case "id":
|
|
56670
|
+
id = value.includes("\0") ? void 0 : value;
|
|
56671
|
+
break;
|
|
56672
|
+
case "retry":
|
|
56673
|
+
/^\d+$/.test(value) ? onRetry(parseInt(value, 10)) : onError(new ParseError(`Invalid \`retry\` value: "${value}"`, {
|
|
56674
|
+
type: "invalid-retry",
|
|
56675
|
+
value,
|
|
56676
|
+
line
|
|
56677
|
+
}));
|
|
56678
|
+
break;
|
|
56679
|
+
default:
|
|
56680
|
+
onError(new ParseError(`Unknown field "${field.length > 20 ? `${field.slice(0, 20)}\u2026` : field}"`, {
|
|
56681
|
+
type: "unknown-field",
|
|
56682
|
+
field,
|
|
56683
|
+
value,
|
|
56684
|
+
line
|
|
56685
|
+
}));
|
|
56686
|
+
break;
|
|
56687
|
+
}
|
|
56688
|
+
}
|
|
56689
|
+
function dispatchEvent() {
|
|
56690
|
+
dataLines > 0 && onEvent({
|
|
56691
|
+
id,
|
|
56692
|
+
event: eventType,
|
|
56693
|
+
data
|
|
56694
|
+
}), id = void 0, data = "", dataLines = 0, eventType = void 0;
|
|
56695
|
+
}
|
|
56696
|
+
function reset(options = {}) {
|
|
56697
|
+
if (options.consume && pendingFragments.length > 0) {
|
|
56698
|
+
const incompleteLine = pendingFragments.join("");
|
|
56699
|
+
parseLine(incompleteLine, 0, incompleteLine.length);
|
|
56700
|
+
}
|
|
56701
|
+
isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, pendingFragments.length = 0, pendingFragmentsLength = 0, terminated = !1;
|
|
56702
|
+
}
|
|
56703
|
+
return {
|
|
56704
|
+
feed,
|
|
56705
|
+
reset
|
|
56706
|
+
};
|
|
56707
|
+
}
|
|
56708
|
+
function isDataPrefix(chunk, i, firstCharCode) {
|
|
56709
|
+
return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58;
|
|
56710
|
+
}
|
|
56711
|
+
function isEventPrefix(chunk, i, firstCharCode) {
|
|
56712
|
+
return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58;
|
|
56713
|
+
}
|
|
56714
|
+
|
|
56715
|
+
//#endregion
|
|
56716
|
+
//#region ../../node_modules/.pnpm/eventsource-parser@3.1.0/node_modules/eventsource-parser/dist/stream.js
|
|
56717
|
+
var EventSourceParserStream = class extends TransformStream {
|
|
56718
|
+
constructor({ onError, onRetry, onComment, maxBufferSize } = {}) {
|
|
56719
|
+
let parser;
|
|
56720
|
+
super({
|
|
56721
|
+
start(controller) {
|
|
56722
|
+
parser = createParser({
|
|
56723
|
+
onEvent: (event) => {
|
|
56724
|
+
controller.enqueue(event);
|
|
56725
|
+
},
|
|
56726
|
+
onError(error) {
|
|
56727
|
+
typeof onError == "function" && onError(error), (onError === "terminate" || error.type === "max-buffer-size-exceeded") && controller.error(error);
|
|
56728
|
+
},
|
|
56729
|
+
onRetry,
|
|
56730
|
+
onComment,
|
|
56731
|
+
maxBufferSize
|
|
56732
|
+
});
|
|
56733
|
+
},
|
|
56734
|
+
transform(chunk) {
|
|
56735
|
+
parser.feed(chunk);
|
|
56736
|
+
}
|
|
56737
|
+
});
|
|
56738
|
+
}
|
|
56739
|
+
};
|
|
56740
|
+
|
|
56741
|
+
//#endregion
|
|
56742
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js
|
|
56743
|
+
const DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS = {
|
|
56744
|
+
initialReconnectionDelay: 1e3,
|
|
56745
|
+
maxReconnectionDelay: 3e4,
|
|
56746
|
+
reconnectionDelayGrowFactor: 1.5,
|
|
56747
|
+
maxRetries: 2
|
|
56748
|
+
};
|
|
56749
|
+
var StreamableHTTPError = class extends Error {
|
|
56750
|
+
constructor(code, message) {
|
|
56751
|
+
super(`Streamable HTTP error: ${message}`);
|
|
56752
|
+
this.code = code;
|
|
56753
|
+
}
|
|
56754
|
+
};
|
|
56755
|
+
/**
|
|
56756
|
+
* Client transport for Streamable HTTP: this implements the MCP Streamable HTTP transport specification.
|
|
56757
|
+
* It will connect to a server using HTTP POST for sending messages and HTTP GET with Server-Sent Events
|
|
56758
|
+
* for receiving messages.
|
|
56759
|
+
*/
|
|
56760
|
+
var StreamableHTTPClientTransport = class {
|
|
56761
|
+
constructor(url, opts) {
|
|
56762
|
+
this._hasCompletedAuthFlow = false;
|
|
56763
|
+
this._url = url;
|
|
56764
|
+
this._resourceMetadataUrl = void 0;
|
|
56765
|
+
this._scope = void 0;
|
|
56766
|
+
this._requestInit = opts?.requestInit;
|
|
56767
|
+
this._authProvider = opts?.authProvider;
|
|
56768
|
+
this._fetch = opts?.fetch;
|
|
56769
|
+
this._fetchWithInit = createFetchWithInit(opts?.fetch, opts?.requestInit);
|
|
56770
|
+
this._sessionId = opts?.sessionId;
|
|
56771
|
+
this._reconnectionOptions = opts?.reconnectionOptions ?? DEFAULT_STREAMABLE_HTTP_RECONNECTION_OPTIONS;
|
|
56772
|
+
}
|
|
56773
|
+
async _authThenStart() {
|
|
56774
|
+
if (!this._authProvider) throw new UnauthorizedError("No auth provider");
|
|
56775
|
+
let result;
|
|
56776
|
+
try {
|
|
56777
|
+
result = await auth(this._authProvider, {
|
|
56778
|
+
serverUrl: this._url,
|
|
56779
|
+
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
56780
|
+
scope: this._scope,
|
|
56781
|
+
fetchFn: this._fetchWithInit
|
|
56782
|
+
});
|
|
56783
|
+
} catch (error) {
|
|
56784
|
+
this.onerror?.(error);
|
|
56785
|
+
throw error;
|
|
56786
|
+
}
|
|
56787
|
+
if (result !== "AUTHORIZED") throw new UnauthorizedError();
|
|
56788
|
+
return await this._startOrAuthSse({ resumptionToken: void 0 });
|
|
56789
|
+
}
|
|
56790
|
+
async _commonHeaders() {
|
|
56791
|
+
const headers = {};
|
|
56792
|
+
if (this._authProvider) {
|
|
56793
|
+
const tokens = await this._authProvider.tokens();
|
|
56794
|
+
if (tokens) headers["Authorization"] = `Bearer ${tokens.access_token}`;
|
|
56795
|
+
}
|
|
56796
|
+
if (this._sessionId) headers["mcp-session-id"] = this._sessionId;
|
|
56797
|
+
if (this._protocolVersion) headers["mcp-protocol-version"] = this._protocolVersion;
|
|
56798
|
+
const extraHeaders = normalizeHeaders(this._requestInit?.headers);
|
|
56799
|
+
return new Headers({
|
|
56800
|
+
...headers,
|
|
56801
|
+
...extraHeaders
|
|
56802
|
+
});
|
|
56803
|
+
}
|
|
56804
|
+
async _startOrAuthSse(options) {
|
|
56805
|
+
const { resumptionToken } = options;
|
|
56806
|
+
try {
|
|
56807
|
+
const headers = await this._commonHeaders();
|
|
56808
|
+
headers.set("Accept", "text/event-stream");
|
|
56809
|
+
if (resumptionToken) headers.set("last-event-id", resumptionToken);
|
|
56810
|
+
const response = await (this._fetch ?? fetch)(this._url, {
|
|
56811
|
+
method: "GET",
|
|
56812
|
+
headers,
|
|
56813
|
+
signal: this._abortController?.signal
|
|
56814
|
+
});
|
|
56815
|
+
if (!response.ok) {
|
|
56816
|
+
await response.body?.cancel();
|
|
56817
|
+
if (response.status === 401 && this._authProvider) return await this._authThenStart();
|
|
56818
|
+
if (response.status === 405) return;
|
|
56819
|
+
throw new StreamableHTTPError(response.status, `Failed to open SSE stream: ${response.statusText}`);
|
|
56820
|
+
}
|
|
56821
|
+
this._handleSseStream(response.body, options, true);
|
|
56822
|
+
} catch (error) {
|
|
56823
|
+
this.onerror?.(error);
|
|
56824
|
+
throw error;
|
|
56825
|
+
}
|
|
56826
|
+
}
|
|
56827
|
+
/**
|
|
56828
|
+
* Calculates the next reconnection delay using backoff algorithm
|
|
56829
|
+
*
|
|
56830
|
+
* @param attempt Current reconnection attempt count for the specific stream
|
|
56831
|
+
* @returns Time to wait in milliseconds before next reconnection attempt
|
|
56832
|
+
*/
|
|
56833
|
+
_getNextReconnectionDelay(attempt) {
|
|
56834
|
+
if (this._serverRetryMs !== void 0) return this._serverRetryMs;
|
|
56835
|
+
const initialDelay = this._reconnectionOptions.initialReconnectionDelay;
|
|
56836
|
+
const growFactor = this._reconnectionOptions.reconnectionDelayGrowFactor;
|
|
56837
|
+
const maxDelay = this._reconnectionOptions.maxReconnectionDelay;
|
|
56838
|
+
return Math.min(initialDelay * Math.pow(growFactor, attempt), maxDelay);
|
|
56839
|
+
}
|
|
56840
|
+
/**
|
|
56841
|
+
* Schedule a reconnection attempt using server-provided retry interval or backoff
|
|
56842
|
+
*
|
|
56843
|
+
* @param lastEventId The ID of the last received event for resumability
|
|
56844
|
+
* @param attemptCount Current reconnection attempt count for this specific stream
|
|
56845
|
+
*/
|
|
56846
|
+
_scheduleReconnection(options, attemptCount = 0) {
|
|
56847
|
+
const maxRetries = this._reconnectionOptions.maxRetries;
|
|
56848
|
+
if (attemptCount >= maxRetries) {
|
|
56849
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Maximum reconnection attempts (${maxRetries}) exceeded.`));
|
|
56850
|
+
return;
|
|
56851
|
+
}
|
|
56852
|
+
const delay = this._getNextReconnectionDelay(attemptCount);
|
|
56853
|
+
this._reconnectionTimeout = setTimeout(() => {
|
|
56854
|
+
this._startOrAuthSse(options).catch((error) => {
|
|
56855
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Failed to reconnect SSE stream: ${error instanceof Error ? error.message : String(error)}`));
|
|
56856
|
+
this._scheduleReconnection(options, attemptCount + 1);
|
|
56857
|
+
});
|
|
56858
|
+
}, delay);
|
|
56859
|
+
}
|
|
56860
|
+
_handleSseStream(stream, options, isReconnectable) {
|
|
56861
|
+
if (!stream) return;
|
|
56862
|
+
const { onresumptiontoken, replayMessageId } = options;
|
|
56863
|
+
let lastEventId;
|
|
56864
|
+
let hasPrimingEvent = false;
|
|
56865
|
+
let receivedResponse = false;
|
|
56866
|
+
const processStream = async () => {
|
|
56867
|
+
try {
|
|
56868
|
+
const reader = stream.pipeThrough(new TextDecoderStream()).pipeThrough(new EventSourceParserStream({ onRetry: (retryMs) => {
|
|
56869
|
+
this._serverRetryMs = retryMs;
|
|
56870
|
+
} })).getReader();
|
|
56871
|
+
while (true) {
|
|
56872
|
+
const { value: event, done } = await reader.read();
|
|
56873
|
+
if (done) break;
|
|
56874
|
+
if (event.id) {
|
|
56875
|
+
lastEventId = event.id;
|
|
56876
|
+
hasPrimingEvent = true;
|
|
56877
|
+
onresumptiontoken?.(event.id);
|
|
56878
|
+
}
|
|
56879
|
+
if (!event.data) continue;
|
|
56880
|
+
if (!event.event || event.event === "message") try {
|
|
56881
|
+
const message = JSONRPCMessageSchema.parse(JSON.parse(event.data));
|
|
56882
|
+
if (isJSONRPCResultResponse(message)) {
|
|
56883
|
+
receivedResponse = true;
|
|
56884
|
+
if (replayMessageId !== void 0) message.id = replayMessageId;
|
|
56885
|
+
}
|
|
56886
|
+
this.onmessage?.(message);
|
|
56887
|
+
} catch (error) {
|
|
56888
|
+
this.onerror?.(error);
|
|
56889
|
+
}
|
|
56890
|
+
}
|
|
56891
|
+
if ((isReconnectable || hasPrimingEvent) && !receivedResponse && this._abortController && !this._abortController.signal.aborted) this._scheduleReconnection({
|
|
56892
|
+
resumptionToken: lastEventId,
|
|
56893
|
+
onresumptiontoken,
|
|
56894
|
+
replayMessageId
|
|
56895
|
+
}, 0);
|
|
56896
|
+
} catch (error) {
|
|
56897
|
+
this.onerror?.(/* @__PURE__ */ new Error(`SSE stream disconnected: ${error}`));
|
|
56898
|
+
if ((isReconnectable || hasPrimingEvent) && !receivedResponse && this._abortController && !this._abortController.signal.aborted) try {
|
|
56899
|
+
this._scheduleReconnection({
|
|
56900
|
+
resumptionToken: lastEventId,
|
|
56901
|
+
onresumptiontoken,
|
|
56902
|
+
replayMessageId
|
|
56903
|
+
}, 0);
|
|
56904
|
+
} catch (error) {
|
|
56905
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Failed to reconnect: ${error instanceof Error ? error.message : String(error)}`));
|
|
56906
|
+
}
|
|
56907
|
+
}
|
|
56908
|
+
};
|
|
56909
|
+
processStream();
|
|
56910
|
+
}
|
|
56911
|
+
async start() {
|
|
56912
|
+
if (this._abortController) throw new Error("StreamableHTTPClientTransport already started! If using Client class, note that connect() calls start() automatically.");
|
|
56913
|
+
this._abortController = new AbortController();
|
|
56914
|
+
}
|
|
56915
|
+
/**
|
|
56916
|
+
* Call this method after the user has finished authorizing via their user agent and is redirected back to the MCP client application. This will exchange the authorization code for an access token, enabling the next connection attempt to successfully auth.
|
|
56917
|
+
*/
|
|
56918
|
+
async finishAuth(authorizationCode) {
|
|
56919
|
+
if (!this._authProvider) throw new UnauthorizedError("No auth provider");
|
|
56920
|
+
if (await auth(this._authProvider, {
|
|
56921
|
+
serverUrl: this._url,
|
|
56922
|
+
authorizationCode,
|
|
56923
|
+
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
56924
|
+
scope: this._scope,
|
|
56925
|
+
fetchFn: this._fetchWithInit
|
|
56926
|
+
}) !== "AUTHORIZED") throw new UnauthorizedError("Failed to authorize");
|
|
56927
|
+
}
|
|
56928
|
+
async close() {
|
|
56929
|
+
if (this._reconnectionTimeout) {
|
|
56930
|
+
clearTimeout(this._reconnectionTimeout);
|
|
56931
|
+
this._reconnectionTimeout = void 0;
|
|
56932
|
+
}
|
|
56933
|
+
this._abortController?.abort();
|
|
56934
|
+
this.onclose?.();
|
|
56935
|
+
}
|
|
56936
|
+
async send(message, options) {
|
|
56937
|
+
try {
|
|
56938
|
+
const { resumptionToken, onresumptiontoken } = options || {};
|
|
56939
|
+
if (resumptionToken) {
|
|
56940
|
+
this._startOrAuthSse({
|
|
56941
|
+
resumptionToken,
|
|
56942
|
+
replayMessageId: isJSONRPCRequest(message) ? message.id : void 0
|
|
56943
|
+
}).catch((err) => this.onerror?.(err));
|
|
56944
|
+
return;
|
|
56945
|
+
}
|
|
56946
|
+
const headers = await this._commonHeaders();
|
|
56947
|
+
headers.set("content-type", "application/json");
|
|
56948
|
+
headers.set("accept", "application/json, text/event-stream");
|
|
56949
|
+
const init = {
|
|
56950
|
+
...this._requestInit,
|
|
56951
|
+
method: "POST",
|
|
56952
|
+
headers,
|
|
56953
|
+
body: JSON.stringify(message),
|
|
56954
|
+
signal: this._abortController?.signal
|
|
56955
|
+
};
|
|
56956
|
+
const response = await (this._fetch ?? fetch)(this._url, init);
|
|
56957
|
+
const sessionId = response.headers.get("mcp-session-id");
|
|
56958
|
+
if (sessionId) this._sessionId = sessionId;
|
|
56959
|
+
if (!response.ok) {
|
|
56960
|
+
const text = await response.text().catch(() => null);
|
|
56961
|
+
if (response.status === 401 && this._authProvider) {
|
|
56962
|
+
if (this._hasCompletedAuthFlow) throw new StreamableHTTPError(401, "Server returned 401 after successful authentication");
|
|
56963
|
+
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response);
|
|
56964
|
+
this._resourceMetadataUrl = resourceMetadataUrl;
|
|
56965
|
+
this._scope = scope;
|
|
56966
|
+
if (await auth(this._authProvider, {
|
|
56967
|
+
serverUrl: this._url,
|
|
56968
|
+
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
56969
|
+
scope: this._scope,
|
|
56970
|
+
fetchFn: this._fetchWithInit
|
|
56971
|
+
}) !== "AUTHORIZED") throw new UnauthorizedError();
|
|
56972
|
+
this._hasCompletedAuthFlow = true;
|
|
56973
|
+
return this.send(message);
|
|
56974
|
+
}
|
|
56975
|
+
if (response.status === 403 && this._authProvider) {
|
|
56976
|
+
const { resourceMetadataUrl, scope, error } = extractWWWAuthenticateParams(response);
|
|
56977
|
+
if (error === "insufficient_scope") {
|
|
56978
|
+
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
|
|
56979
|
+
if (this._lastUpscopingHeader === wwwAuthHeader) throw new StreamableHTTPError(403, "Server returned 403 after trying upscoping");
|
|
56980
|
+
if (scope) this._scope = scope;
|
|
56981
|
+
if (resourceMetadataUrl) this._resourceMetadataUrl = resourceMetadataUrl;
|
|
56982
|
+
this._lastUpscopingHeader = wwwAuthHeader ?? void 0;
|
|
56983
|
+
if (await auth(this._authProvider, {
|
|
56984
|
+
serverUrl: this._url,
|
|
56985
|
+
resourceMetadataUrl: this._resourceMetadataUrl,
|
|
56986
|
+
scope: this._scope,
|
|
56987
|
+
fetchFn: this._fetch
|
|
56988
|
+
}) !== "AUTHORIZED") throw new UnauthorizedError();
|
|
56989
|
+
return this.send(message);
|
|
56990
|
+
}
|
|
56991
|
+
}
|
|
56992
|
+
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
|
56993
|
+
}
|
|
56994
|
+
this._hasCompletedAuthFlow = false;
|
|
56995
|
+
this._lastUpscopingHeader = void 0;
|
|
56996
|
+
if (response.status === 202) {
|
|
56997
|
+
await response.body?.cancel();
|
|
56998
|
+
if (isInitializedNotification(message)) this._startOrAuthSse({ resumptionToken: void 0 }).catch((err) => this.onerror?.(err));
|
|
56999
|
+
return;
|
|
57000
|
+
}
|
|
57001
|
+
const hasRequests = (Array.isArray(message) ? message : [message]).filter((msg) => "method" in msg && "id" in msg && msg.id !== void 0).length > 0;
|
|
57002
|
+
const contentType = response.headers.get("content-type");
|
|
57003
|
+
if (hasRequests) if (contentType?.includes("text/event-stream")) this._handleSseStream(response.body, { onresumptiontoken }, false);
|
|
57004
|
+
else if (contentType?.includes("application/json")) {
|
|
57005
|
+
const data = await response.json();
|
|
57006
|
+
const responseMessages = Array.isArray(data) ? data.map((msg) => JSONRPCMessageSchema.parse(msg)) : [JSONRPCMessageSchema.parse(data)];
|
|
57007
|
+
for (const msg of responseMessages) this.onmessage?.(msg);
|
|
57008
|
+
} else {
|
|
57009
|
+
await response.body?.cancel();
|
|
57010
|
+
throw new StreamableHTTPError(-1, `Unexpected content type: ${contentType}`);
|
|
57011
|
+
}
|
|
57012
|
+
else await response.body?.cancel();
|
|
57013
|
+
} catch (error) {
|
|
57014
|
+
this.onerror?.(error);
|
|
57015
|
+
throw error;
|
|
57016
|
+
}
|
|
57017
|
+
}
|
|
57018
|
+
get sessionId() {
|
|
57019
|
+
return this._sessionId;
|
|
57020
|
+
}
|
|
57021
|
+
/**
|
|
57022
|
+
* Terminates the current session by sending a DELETE request to the server.
|
|
57023
|
+
*
|
|
57024
|
+
* Clients that no longer need a particular session
|
|
57025
|
+
* (e.g., because the user is leaving the client application) SHOULD send an
|
|
57026
|
+
* HTTP DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
|
|
57027
|
+
* terminate the session.
|
|
57028
|
+
*
|
|
57029
|
+
* The server MAY respond with HTTP 405 Method Not Allowed, indicating that
|
|
57030
|
+
* the server does not allow clients to terminate sessions.
|
|
57031
|
+
*/
|
|
57032
|
+
async terminateSession() {
|
|
57033
|
+
if (!this._sessionId) return;
|
|
57034
|
+
try {
|
|
57035
|
+
const headers = await this._commonHeaders();
|
|
57036
|
+
const init = {
|
|
57037
|
+
...this._requestInit,
|
|
57038
|
+
method: "DELETE",
|
|
57039
|
+
headers,
|
|
57040
|
+
signal: this._abortController?.signal
|
|
57041
|
+
};
|
|
57042
|
+
const response = await (this._fetch ?? fetch)(this._url, init);
|
|
57043
|
+
await response.body?.cancel();
|
|
57044
|
+
if (!response.ok && response.status !== 405) throw new StreamableHTTPError(response.status, `Failed to terminate session: ${response.statusText}`);
|
|
57045
|
+
this._sessionId = void 0;
|
|
57046
|
+
} catch (error) {
|
|
57047
|
+
this.onerror?.(error);
|
|
57048
|
+
throw error;
|
|
57049
|
+
}
|
|
57050
|
+
}
|
|
57051
|
+
setProtocolVersion(version) {
|
|
57052
|
+
this._protocolVersion = version;
|
|
57053
|
+
}
|
|
57054
|
+
get protocolVersion() {
|
|
57055
|
+
return this._protocolVersion;
|
|
57056
|
+
}
|
|
57057
|
+
/**
|
|
57058
|
+
* Resume an SSE stream from a previous event ID.
|
|
57059
|
+
* Opens a GET SSE connection with Last-Event-ID header to replay missed events.
|
|
57060
|
+
*
|
|
57061
|
+
* @param lastEventId The event ID to resume from
|
|
57062
|
+
* @param options Optional callback to receive new resumption tokens
|
|
57063
|
+
*/
|
|
57064
|
+
async resumeStream(lastEventId, options) {
|
|
57065
|
+
await this._startOrAuthSse({
|
|
57066
|
+
resumptionToken: lastEventId,
|
|
57067
|
+
onresumptiontoken: options?.onresumptiontoken
|
|
57068
|
+
});
|
|
57069
|
+
}
|
|
57070
|
+
};
|
|
57071
|
+
|
|
57072
|
+
//#endregion
|
|
57073
|
+
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/inMemory.js
|
|
57074
|
+
/**
|
|
57075
|
+
* In-memory transport for creating clients and servers that talk to each other within the same process.
|
|
57076
|
+
*/
|
|
57077
|
+
var InMemoryTransport = class InMemoryTransport {
|
|
57078
|
+
constructor() {
|
|
57079
|
+
this._messageQueue = [];
|
|
57080
|
+
}
|
|
57081
|
+
/**
|
|
57082
|
+
* Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a Client and one to a Server.
|
|
57083
|
+
*/
|
|
57084
|
+
static createLinkedPair() {
|
|
57085
|
+
const clientTransport = new InMemoryTransport();
|
|
57086
|
+
const serverTransport = new InMemoryTransport();
|
|
57087
|
+
clientTransport._otherTransport = serverTransport;
|
|
57088
|
+
serverTransport._otherTransport = clientTransport;
|
|
57089
|
+
return [clientTransport, serverTransport];
|
|
57090
|
+
}
|
|
57091
|
+
async start() {
|
|
57092
|
+
while (this._messageQueue.length > 0) {
|
|
57093
|
+
const queuedMessage = this._messageQueue.shift();
|
|
57094
|
+
this.onmessage?.(queuedMessage.message, queuedMessage.extra);
|
|
57095
|
+
}
|
|
57096
|
+
}
|
|
57097
|
+
async close() {
|
|
57098
|
+
const other = this._otherTransport;
|
|
57099
|
+
this._otherTransport = void 0;
|
|
57100
|
+
await other?.close();
|
|
57101
|
+
this.onclose?.();
|
|
57102
|
+
}
|
|
57103
|
+
/**
|
|
57104
|
+
* Sends a message with optional auth info.
|
|
57105
|
+
* This is useful for testing authentication scenarios.
|
|
57106
|
+
*/
|
|
57107
|
+
async send(message, options) {
|
|
57108
|
+
if (!this._otherTransport) throw new Error("Not connected");
|
|
57109
|
+
if (this._otherTransport.onmessage) this._otherTransport.onmessage(message, { authInfo: options?.authInfo });
|
|
57110
|
+
else this._otherTransport._messageQueue.push({
|
|
57111
|
+
message,
|
|
57112
|
+
extra: { authInfo: options?.authInfo }
|
|
57113
|
+
});
|
|
57114
|
+
}
|
|
57115
|
+
};
|
|
57116
|
+
|
|
54875
57117
|
//#endregion
|
|
54876
57118
|
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/server.js
|
|
54877
57119
|
/**
|
|
@@ -55093,58 +57335,6 @@ var ExperimentalServerTasks = class {
|
|
|
55093
57335
|
}
|
|
55094
57336
|
};
|
|
55095
57337
|
|
|
55096
|
-
//#endregion
|
|
55097
|
-
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/helpers.js
|
|
55098
|
-
/**
|
|
55099
|
-
* Experimental task capability assertion helpers.
|
|
55100
|
-
* WARNING: These APIs are experimental and may change without notice.
|
|
55101
|
-
*
|
|
55102
|
-
* @experimental
|
|
55103
|
-
*/
|
|
55104
|
-
/**
|
|
55105
|
-
* Asserts that task creation is supported for tools/call.
|
|
55106
|
-
* Used by Client.assertTaskCapability and Server.assertTaskHandlerCapability.
|
|
55107
|
-
*
|
|
55108
|
-
* @param requests - The task requests capability object
|
|
55109
|
-
* @param method - The method being checked
|
|
55110
|
-
* @param entityName - 'Server' or 'Client' for error messages
|
|
55111
|
-
* @throws Error if the capability is not supported
|
|
55112
|
-
*
|
|
55113
|
-
* @experimental
|
|
55114
|
-
*/
|
|
55115
|
-
function assertToolsCallTaskCapability(requests, method, entityName) {
|
|
55116
|
-
if (!requests) throw new Error(`${entityName} does not support task creation (required for ${method})`);
|
|
55117
|
-
switch (method) {
|
|
55118
|
-
case "tools/call":
|
|
55119
|
-
if (!requests.tools?.call) throw new Error(`${entityName} does not support task creation for tools/call (required for ${method})`);
|
|
55120
|
-
break;
|
|
55121
|
-
default: break;
|
|
55122
|
-
}
|
|
55123
|
-
}
|
|
55124
|
-
/**
|
|
55125
|
-
* Asserts that task creation is supported for sampling/createMessage or elicitation/create.
|
|
55126
|
-
* Used by Server.assertTaskCapability and Client.assertTaskHandlerCapability.
|
|
55127
|
-
*
|
|
55128
|
-
* @param requests - The task requests capability object
|
|
55129
|
-
* @param method - The method being checked
|
|
55130
|
-
* @param entityName - 'Server' or 'Client' for error messages
|
|
55131
|
-
* @throws Error if the capability is not supported
|
|
55132
|
-
*
|
|
55133
|
-
* @experimental
|
|
55134
|
-
*/
|
|
55135
|
-
function assertClientRequestTaskCapability(requests, method, entityName) {
|
|
55136
|
-
if (!requests) throw new Error(`${entityName} does not support task creation (required for ${method})`);
|
|
55137
|
-
switch (method) {
|
|
55138
|
-
case "sampling/createMessage":
|
|
55139
|
-
if (!requests.sampling?.createMessage) throw new Error(`${entityName} does not support task creation for sampling/createMessage (required for ${method})`);
|
|
55140
|
-
break;
|
|
55141
|
-
case "elicitation/create":
|
|
55142
|
-
if (!requests.elicitation?.create) throw new Error(`${entityName} does not support task creation for elicitation/create (required for ${method})`);
|
|
55143
|
-
break;
|
|
55144
|
-
default: break;
|
|
55145
|
-
}
|
|
55146
|
-
}
|
|
55147
|
-
|
|
55148
57338
|
//#endregion
|
|
55149
57339
|
//#region ../../node_modules/.pnpm/@modelcontextprotocol+sdk@1.29.0_zod@4.4.3/node_modules/@modelcontextprotocol/sdk/dist/esm/server/index.js
|
|
55150
57340
|
/**
|
|
@@ -56241,6 +58431,26 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
56241
58431
|
//#endregion
|
|
56242
58432
|
//#region ../mcp-microsoft-planner-premium/dist/index.mjs
|
|
56243
58433
|
/**
|
|
58434
|
+
* The rhythm, fixed. The caller chooses how long to wait; it never chooses how often
|
|
58435
|
+
* to look, because that is a property of the API's latency rather than of anything a
|
|
58436
|
+
* caller knows. The recorder's sequences come out at this cadence for the better reason:
|
|
58437
|
+
* it drives `waitForOperationSet` rather than polling by hand.
|
|
58438
|
+
*/
|
|
58439
|
+
const POLL_SECONDS = 4;
|
|
58440
|
+
/**
|
|
58441
|
+
* The order the verbs are staged in — creates, then updates, then deletes.
|
|
58442
|
+
*
|
|
58443
|
+
* Fixed rather than derived from the caller's list, because it is the order that was
|
|
58444
|
+
* driven against a real org and observed applying whole. Whether the service would
|
|
58445
|
+
* honour another order is unknown and not worth discovering by accident, and a
|
|
58446
|
+
* caller has no reason to care: the set is one transaction either way.
|
|
58447
|
+
*/
|
|
58448
|
+
const STAGING_ORDER = [
|
|
58449
|
+
"create",
|
|
58450
|
+
"update",
|
|
58451
|
+
"delete"
|
|
58452
|
+
];
|
|
58453
|
+
/**
|
|
56244
58454
|
* When to read, in seconds from the call: immediately, every `POLL_SECONDS`, and at
|
|
56245
58455
|
* the closing bell. A list rather than a loop condition — the schedule is data, so
|
|
56246
58456
|
* it can be read, tested, and argued about without a clock anywhere near it.
|
|
@@ -56251,7 +58461,7 @@ const EMPTY_COMPLETION_RESULT = { completion: {
|
|
|
56251
58461
|
*/
|
|
56252
58462
|
function readInstants(seconds) {
|
|
56253
58463
|
const instants = [0];
|
|
56254
|
-
for (let at =
|
|
58464
|
+
for (let at = POLL_SECONDS; at < seconds; at += POLL_SECONDS) instants.push(at);
|
|
56255
58465
|
if (seconds > 0) instants.push(seconds);
|
|
56256
58466
|
return instants;
|
|
56257
58467
|
}
|
|
@@ -56259,11 +58469,11 @@ function reason(error) {
|
|
|
56259
58469
|
return error instanceof Error ? error.message : String(error);
|
|
56260
58470
|
}
|
|
56261
58471
|
/**
|
|
56262
|
-
* Submit one
|
|
58472
|
+
* Submit one operation set and hand back the receipt.
|
|
56263
58473
|
*
|
|
56264
|
-
* ABANDON ONLY ON A STAGING FAILURE. A set whose staging threw holds
|
|
56265
|
-
* still open, so it would sit against the caller's open-set quota
|
|
56266
|
-
* API to find it again; abandoning
|
|
58474
|
+
* ABANDON ONLY ON A STAGING FAILURE. A set whose staging threw holds a partial
|
|
58475
|
+
* transaction and is still open, so it would sit against the caller's open-set quota
|
|
58476
|
+
* forever with no API to find it again; abandoning it is accepted. After execute the
|
|
56267
58477
|
* question does not arise — this returns the moment the service accepts, and what
|
|
56268
58478
|
* happens to the set afterwards belongs to `waitForOperationSet`.
|
|
56269
58479
|
*
|
|
@@ -56271,17 +58481,32 @@ function reason(error) {
|
|
|
56271
58481
|
* half true: a set closes when the WORK fails and stays OPEN when the SET itself is
|
|
56272
58482
|
* rejected (an empty one, for instance). The rule above holds either way; the
|
|
56273
58483
|
* plausible-sounding premise does not.
|
|
58484
|
+
*
|
|
58485
|
+
* An EMPTY submission is the caller's to prevent. Executing a set with nothing
|
|
58486
|
+
* staged is rejected AND leaves the set open — the one execute failure that leaks a
|
|
58487
|
+
* quota slot — so the edge refuses it before a set is ever minted, rather than this
|
|
58488
|
+
* layer growing a branch for a call that should not have been made.
|
|
56274
58489
|
*/
|
|
56275
|
-
async function
|
|
58490
|
+
async function submitOperationSet(api, input) {
|
|
56276
58491
|
const operationSetId = await api.createOperationSet({
|
|
56277
58492
|
description: input.description,
|
|
56278
58493
|
projectId: input.projectId
|
|
56279
58494
|
});
|
|
56280
58495
|
try {
|
|
56281
|
-
|
|
56282
|
-
|
|
56283
|
-
|
|
56284
|
-
|
|
58496
|
+
const entitiesByVerb = {
|
|
58497
|
+
create: [],
|
|
58498
|
+
delete: [],
|
|
58499
|
+
update: []
|
|
58500
|
+
};
|
|
58501
|
+
for (const write of input.writes) entitiesByVerb[write.verb].push(write.entity);
|
|
58502
|
+
for (const verb of STAGING_ORDER) {
|
|
58503
|
+
const entities = entitiesByVerb[verb];
|
|
58504
|
+
if (entities.length > 0) await api.stage({
|
|
58505
|
+
entities,
|
|
58506
|
+
operationSetId,
|
|
58507
|
+
verb
|
|
58508
|
+
});
|
|
58509
|
+
}
|
|
56285
58510
|
} catch (error) {
|
|
56286
58511
|
try {
|
|
56287
58512
|
await api.abandonOperationSet(operationSetId);
|
|
@@ -56313,6 +58538,9 @@ async function outcomeOf(api, set) {
|
|
|
56313
58538
|
* Lifting it out is what makes the two exits the same line of code: whether the set
|
|
56314
58539
|
* settled or the caller simply ran out of window, the answer is the last thing the
|
|
56315
58540
|
* set actually said.
|
|
58541
|
+
*
|
|
58542
|
+
* `executing` is not a promise of settlement. When the window closes, return the
|
|
58543
|
+
* last observed status rather than timing out and inviting a non-idempotent retry.
|
|
56316
58544
|
*/
|
|
56317
58545
|
async function waitForOperationSet(api, input) {
|
|
56318
58546
|
let elapsed = 0;
|
|
@@ -56325,6 +58553,56 @@ async function waitForOperationSet(api, input) {
|
|
|
56325
58553
|
}
|
|
56326
58554
|
return outcomeOf(api, set);
|
|
56327
58555
|
}
|
|
58556
|
+
/**
|
|
58557
|
+
* The record kinds this package can write, and the two wire facts each one needs:
|
|
58558
|
+
* its `@odata.type` and the column holding its primary key. One table rather than a
|
|
58559
|
+
* literal per builder, because a delete names a record by exactly these two things
|
|
58560
|
+
* and would otherwise have to restate every one of them.
|
|
58561
|
+
*/
|
|
58562
|
+
const RECORD = {
|
|
58563
|
+
assignment: {
|
|
58564
|
+
id: "msdyn_resourceassignmentid",
|
|
58565
|
+
type: "Microsoft.Dynamics.CRM.msdyn_resourceassignment"
|
|
58566
|
+
},
|
|
58567
|
+
bucket: {
|
|
58568
|
+
id: "msdyn_projectbucketid",
|
|
58569
|
+
type: "Microsoft.Dynamics.CRM.msdyn_projectbucket"
|
|
58570
|
+
},
|
|
58571
|
+
label: {
|
|
58572
|
+
id: "msdyn_projectlabelid",
|
|
58573
|
+
type: "Microsoft.Dynamics.CRM.msdyn_projectlabel"
|
|
58574
|
+
},
|
|
58575
|
+
task: {
|
|
58576
|
+
id: "msdyn_projecttaskid",
|
|
58577
|
+
type: "Microsoft.Dynamics.CRM.msdyn_projecttask"
|
|
58578
|
+
},
|
|
58579
|
+
taskLabel: {
|
|
58580
|
+
id: "msdyn_projecttasktolabelid",
|
|
58581
|
+
type: "Microsoft.Dynamics.CRM.msdyn_projecttasktolabel"
|
|
58582
|
+
}
|
|
58583
|
+
};
|
|
58584
|
+
const identify = (kind, id) => ({
|
|
58585
|
+
"@odata.type": RECORD[kind].type,
|
|
58586
|
+
[RECORD[kind].id]: id
|
|
58587
|
+
});
|
|
58588
|
+
/**
|
|
58589
|
+
* A lookup, in OData's reference syntax.
|
|
58590
|
+
*
|
|
58591
|
+
* The navigation property is NOT always the column that holds the lookup, and
|
|
58592
|
+
* Dataverse matches it case-sensitively: a task binds its project through
|
|
58593
|
+
* `msdyn_project` but a task-label link binds its task through `msdyn_ProjectTaskId`.
|
|
58594
|
+
* Every name passed here comes from the org's own `$metadata`, never from the
|
|
58595
|
+
* column name it resembles.
|
|
58596
|
+
*/
|
|
58597
|
+
const bind = (navigationProperty, entitySet, id) => id === void 0 ? {} : { [`${navigationProperty}@odata.bind`]: `/${entitySet}(${id})` };
|
|
58598
|
+
/**
|
|
58599
|
+
* The entity, minus the columns the caller did not name. THIS is what makes a partial
|
|
58600
|
+
* update partial.
|
|
58601
|
+
*
|
|
58602
|
+
* `undefined` and absent are the same thing to this API; a column is written or it is
|
|
58603
|
+
* not, and there is no third state to lose here.
|
|
58604
|
+
*/
|
|
58605
|
+
const written = (entity) => Object.fromEntries(Object.entries(entity).filter(([, value]) => value !== void 0));
|
|
56328
58606
|
const OPERATION_SET_STATUS = {
|
|
56329
58607
|
19235e4: "open",
|
|
56330
58608
|
192350001: "executing",
|
|
@@ -56361,50 +58639,188 @@ const pssErrorLogRowSchema = object$4({
|
|
|
56361
58639
|
});
|
|
56362
58640
|
const createOperationSetResponseSchema = object$4({ OperationSetId: string$3().min(1) });
|
|
56363
58641
|
const SET_COLUMNS = "?$select=msdyn_status,_msdyn_psserrorlog_value";
|
|
56364
|
-
const projectTaskType = "Microsoft.Dynamics.CRM.msdyn_projecttask";
|
|
56365
|
-
const bucketBind = (bucketId) => ({ "msdyn_projectbucket@odata.bind": `/msdyn_projectbuckets(${bucketId})` });
|
|
56366
58642
|
/**
|
|
56367
|
-
*
|
|
56368
|
-
*
|
|
56369
|
-
* `
|
|
56370
|
-
*
|
|
58643
|
+
* The task columns a create and an update spell identically.
|
|
58644
|
+
*
|
|
58645
|
+
* `msdyn_description` only — NEVER `msdyn_descriptionplaintext`. Writing the first
|
|
58646
|
+
* populates the second; writing the second directly is refused. A caller that set
|
|
58647
|
+
* both would be sending one column the service rejects, for nothing.
|
|
58648
|
+
*
|
|
58649
|
+
* Dates are forwarded unchanged. Callers must provide an unambiguous instant; this
|
|
58650
|
+
* layer does not normalize because doing so would guess their intent.
|
|
58651
|
+
*/
|
|
58652
|
+
function taskColumns(fields) {
|
|
58653
|
+
return {
|
|
58654
|
+
msdyn_description: fields.description,
|
|
58655
|
+
msdyn_finish: fields.finish,
|
|
58656
|
+
msdyn_priority: fields.priority,
|
|
58657
|
+
msdyn_start: fields.start
|
|
58658
|
+
};
|
|
58659
|
+
}
|
|
58660
|
+
/**
|
|
58661
|
+
* A NEW project task.
|
|
58662
|
+
*
|
|
58663
|
+
* The primary key is OURS to choose: the service honours a client-supplied
|
|
58664
|
+
* `msdyn_projecttaskid`. That is what lets a caller know the id before the write has
|
|
58665
|
+
* been executed — and what lets this task name a BUCKET created in the same batch.
|
|
58666
|
+
*
|
|
58667
|
+
* `outlineLevel` is the caller's to state rather than ours to default. A subtask sits
|
|
58668
|
+
* one level below its parent, and this file cannot learn a parent's level without a
|
|
58669
|
+
* read it has no business making; the agent has that column in the synced tables.
|
|
56371
58670
|
*/
|
|
56372
58671
|
function createProjectTask(fields) {
|
|
58672
|
+
return {
|
|
58673
|
+
entity: written({
|
|
58674
|
+
...identify("task", fields.taskId),
|
|
58675
|
+
msdyn_outlinelevel: fields.outlineLevel,
|
|
58676
|
+
msdyn_subject: fields.subject,
|
|
58677
|
+
...bind("msdyn_project", "msdyn_projects", fields.projectId),
|
|
58678
|
+
...bind("msdyn_projectbucket", "msdyn_projectbuckets", fields.bucketId),
|
|
58679
|
+
...bind("msdyn_parenttask", "msdyn_projecttasks", fields.parentTaskId),
|
|
58680
|
+
...taskColumns(fields)
|
|
58681
|
+
}),
|
|
58682
|
+
verb: "create"
|
|
58683
|
+
};
|
|
58684
|
+
}
|
|
58685
|
+
/**
|
|
58686
|
+
* A CHANGE to an existing task: only the named columns are written, which is what
|
|
58687
|
+
* makes a partial update partial. The project is deliberately not expressible — a
|
|
58688
|
+
* change cannot move a task to another project.
|
|
58689
|
+
*
|
|
58690
|
+
* Cross-project moves are deliberately unsupported because they can silently mutate
|
|
58691
|
+
* unintended data rather than fail.
|
|
58692
|
+
*
|
|
58693
|
+
* Date updates can reschedule dependent tasks. This builder forwards them without
|
|
58694
|
+
* attempting impact analysis.
|
|
58695
|
+
*/
|
|
58696
|
+
function updateProjectTask(fields) {
|
|
58697
|
+
return {
|
|
58698
|
+
entity: written({
|
|
58699
|
+
...identify("task", fields.taskId),
|
|
58700
|
+
msdyn_duration: fields.duration,
|
|
58701
|
+
msdyn_effort: fields.effort,
|
|
58702
|
+
msdyn_progress: fields.progress,
|
|
58703
|
+
msdyn_subject: fields.subject,
|
|
58704
|
+
...bind("msdyn_projectbucket", "msdyn_projectbuckets", fields.bucketId),
|
|
58705
|
+
...bind("msdyn_parenttask", "msdyn_projecttasks", fields.parentTaskId),
|
|
58706
|
+
...taskColumns(fields)
|
|
58707
|
+
}),
|
|
58708
|
+
verb: "update"
|
|
58709
|
+
};
|
|
58710
|
+
}
|
|
58711
|
+
/**
|
|
58712
|
+
* A NEW bucket on a project.
|
|
58713
|
+
*
|
|
58714
|
+
* Display order is deliberately unsupported; the recorder's create display-order
|
|
58715
|
+
* probe owns the service claim behind that omission.
|
|
58716
|
+
*/
|
|
58717
|
+
function createProjectBucket(fields) {
|
|
56373
58718
|
return {
|
|
56374
58719
|
entity: {
|
|
56375
|
-
"
|
|
56376
|
-
|
|
56377
|
-
"msdyn_project
|
|
56378
|
-
...bucketBind(fields.bucketId),
|
|
56379
|
-
msdyn_projecttaskid: fields.taskId,
|
|
56380
|
-
msdyn_subject: fields.subject
|
|
58720
|
+
...identify("bucket", fields.bucketId),
|
|
58721
|
+
msdyn_name: fields.name,
|
|
58722
|
+
...bind("msdyn_project", "msdyn_projects", fields.projectId)
|
|
56381
58723
|
},
|
|
56382
58724
|
verb: "create"
|
|
56383
58725
|
};
|
|
56384
58726
|
}
|
|
56385
58727
|
/**
|
|
56386
|
-
*
|
|
56387
|
-
* makes a partial update partial. Neither the project nor the outline is expressible
|
|
56388
|
-
* — a change cannot reparent or restructure.
|
|
58728
|
+
* Rename an existing bucket.
|
|
56389
58729
|
*
|
|
56390
|
-
*
|
|
56391
|
-
*
|
|
56392
|
-
* reports the set `completed` and writes a third bucket, neither the one requested
|
|
56393
|
-
* nor the one the task was in. Withholding the project change is not withholding a
|
|
56394
|
-
* rejection.
|
|
58730
|
+
* Reordering is deliberately unsupported; the recorder's update display-order probe
|
|
58731
|
+
* owns the service claim behind that omission.
|
|
56395
58732
|
*/
|
|
56396
|
-
function
|
|
58733
|
+
function updateProjectBucket(fields) {
|
|
58734
|
+
return {
|
|
58735
|
+
entity: {
|
|
58736
|
+
...identify("bucket", fields.bucketId),
|
|
58737
|
+
msdyn_name: fields.name
|
|
58738
|
+
},
|
|
58739
|
+
verb: "update"
|
|
58740
|
+
};
|
|
58741
|
+
}
|
|
58742
|
+
/**
|
|
58743
|
+
* Rename a label. Its colour is not ours to change.
|
|
58744
|
+
*
|
|
58745
|
+
* A label is never CREATED, and the API offers no way to: every project is born with
|
|
58746
|
+
* twenty-five of them, unnamed, one per colour. So the only way to have a label
|
|
58747
|
+
* called "Blocked" is to rename one that already exists, and the only way to be rid
|
|
58748
|
+
* of one is to clear its text.
|
|
58749
|
+
*
|
|
58750
|
+
* Colour is deliberately unsupported; the recorder's label-colour probe owns the
|
|
58751
|
+
* service claim behind that omission.
|
|
58752
|
+
*/
|
|
58753
|
+
function updateProjectLabel(fields) {
|
|
56397
58754
|
return {
|
|
56398
58755
|
entity: {
|
|
56399
|
-
"
|
|
56400
|
-
|
|
56401
|
-
...fields.progress === void 0 ? {} : { msdyn_progress: fields.progress },
|
|
56402
|
-
...fields.bucketId === void 0 ? {} : bucketBind(fields.bucketId),
|
|
56403
|
-
...fields.subject === void 0 ? {} : { msdyn_subject: fields.subject }
|
|
58756
|
+
...identify("label", fields.labelId),
|
|
58757
|
+
msdyn_projectlabeltext: fields.text
|
|
56404
58758
|
},
|
|
56405
58759
|
verb: "update"
|
|
56406
58760
|
};
|
|
56407
58761
|
}
|
|
58762
|
+
/**
|
|
58763
|
+
* Put a label on a task. A join row — so taking the label off is deleting this row,
|
|
58764
|
+
* which is why there is no `updateTaskLabel` to go with it.
|
|
58765
|
+
*/
|
|
58766
|
+
function createTaskLabel(fields) {
|
|
58767
|
+
return {
|
|
58768
|
+
entity: {
|
|
58769
|
+
...identify("taskLabel", fields.linkId),
|
|
58770
|
+
...bind("msdyn_ProjectTaskId", "msdyn_projecttasks", fields.taskId),
|
|
58771
|
+
...bind("msdyn_ProjectLabelId", "msdyn_projectlabels", fields.labelId)
|
|
58772
|
+
},
|
|
58773
|
+
verb: "create"
|
|
58774
|
+
};
|
|
58775
|
+
}
|
|
58776
|
+
/**
|
|
58777
|
+
* Assign a project team member to a task.
|
|
58778
|
+
*
|
|
58779
|
+
* An assignment inherits its task's dates and identifies the assignee by project-team
|
|
58780
|
+
* membership.
|
|
58781
|
+
*/
|
|
58782
|
+
function createResourceAssignment(fields) {
|
|
58783
|
+
return {
|
|
58784
|
+
entity: {
|
|
58785
|
+
...identify("assignment", fields.assignmentId),
|
|
58786
|
+
msdyn_name: fields.name,
|
|
58787
|
+
...bind("msdyn_taskid", "msdyn_projecttasks", fields.taskId),
|
|
58788
|
+
...bind("msdyn_projectid", "msdyn_projects", fields.projectId),
|
|
58789
|
+
...bind("msdyn_projectteamid", "msdyn_projectteams", fields.projectTeamId)
|
|
58790
|
+
},
|
|
58791
|
+
verb: "create"
|
|
58792
|
+
};
|
|
58793
|
+
}
|
|
58794
|
+
/**
|
|
58795
|
+
* Remove a record. A staged delete names its record exactly as a create does — type
|
|
58796
|
+
* and id — so one shape covers every kind this package can write.
|
|
58797
|
+
*/
|
|
58798
|
+
function deleteRecord(fields) {
|
|
58799
|
+
return {
|
|
58800
|
+
entity: identify(fields.kind, fields.id),
|
|
58801
|
+
verb: "delete"
|
|
58802
|
+
};
|
|
58803
|
+
}
|
|
58804
|
+
/** A non-2xx answer from the Schedule API, distinct from local/schema failures. */
|
|
58805
|
+
var ScheduleApiError = class extends Error {
|
|
58806
|
+
path;
|
|
58807
|
+
status;
|
|
58808
|
+
statusText;
|
|
58809
|
+
responseBody;
|
|
58810
|
+
name = "ScheduleApiError";
|
|
58811
|
+
constructor(path, status, statusText, responseBody) {
|
|
58812
|
+
super(`Schedule API ${path} failed: ${status} ${statusText}${responseBody ? ` — ${responseBody}` : ""}`);
|
|
58813
|
+
this.path = path;
|
|
58814
|
+
this.status = status;
|
|
58815
|
+
this.statusText = statusText;
|
|
58816
|
+
this.responseBody = responseBody;
|
|
58817
|
+
}
|
|
58818
|
+
};
|
|
58819
|
+
const STAGE_ACTION = {
|
|
58820
|
+
create: "msdyn_PssCreateV2",
|
|
58821
|
+
delete: "msdyn_PssDeleteV2",
|
|
58822
|
+
update: "msdyn_PssUpdateV2"
|
|
58823
|
+
};
|
|
56408
58824
|
function createScheduleApiClient(config) {
|
|
56409
58825
|
async function hop(path, init) {
|
|
56410
58826
|
const response = await fetch(`${config.orgUrl}/api/data/v9.2/${path}`, {
|
|
@@ -56417,7 +58833,7 @@ function createScheduleApiClient(config) {
|
|
|
56417
58833
|
});
|
|
56418
58834
|
if (!response.ok) {
|
|
56419
58835
|
const detail = await response.text().catch(() => "");
|
|
56420
|
-
throw new
|
|
58836
|
+
throw new ScheduleApiError(path, response.status, response.statusText, detail);
|
|
56421
58837
|
}
|
|
56422
58838
|
return await response.text();
|
|
56423
58839
|
}
|
|
@@ -56455,30 +58871,143 @@ function createScheduleApiClient(config) {
|
|
|
56455
58871
|
log: row.msdyn_log ?? ""
|
|
56456
58872
|
};
|
|
56457
58873
|
},
|
|
56458
|
-
stage: (input) => action(input.
|
|
56459
|
-
|
|
58874
|
+
stage: (input) => action(STAGE_ACTION[input.verb], {
|
|
58875
|
+
EntityCollection: input.entities,
|
|
56460
58876
|
OperationSetId: input.operationSetId
|
|
56461
58877
|
})
|
|
56462
58878
|
};
|
|
56463
58879
|
}
|
|
56464
|
-
const SERVER_VERSION = "0.5.
|
|
58880
|
+
const SERVER_VERSION = "0.5.16";
|
|
58881
|
+
/** The most operations one OperationSet will carry. The service's own ceiling. */
|
|
58882
|
+
const MAX_OPERATIONS = 200;
|
|
56465
58883
|
/**
|
|
56466
|
-
* A Dataverse row as the
|
|
56467
|
-
*
|
|
56468
|
-
*
|
|
56469
|
-
*
|
|
56470
|
-
* already approved.
|
|
58884
|
+
* A Dataverse row as the tools take it: the GUID does the work, the name is what the
|
|
58885
|
+
* human reads on the approval card. They are ONE object because they are one concept
|
|
58886
|
+
* — which makes "an id without its label" unrepresentable in the advertised schema,
|
|
58887
|
+
* rather than a runtime check that fires after a human has already approved.
|
|
56471
58888
|
*
|
|
56472
58889
|
* `guid()`, NOT `uuid()`: Dataverse GUIDs are not RFC-4122, so msdyn_* ids routinely
|
|
56473
58890
|
* carry a non-version nibble (`…-f011-…`) that zod's `uuid()` rejects — a `uuid()`
|
|
56474
|
-
* here silently fails every real
|
|
56475
|
-
*
|
|
58891
|
+
* here silently fails every real write at argument validation. `guid()` still
|
|
58892
|
+
* enforces the 8-4-4-4-12 shape. Do not tighten.
|
|
56476
58893
|
*/
|
|
56477
|
-
const
|
|
58894
|
+
const existing = (what, idColumn) => strictObject({
|
|
56478
58895
|
id: guid$1().describe(`${what} GUID from the synced Postgres tables (${idColumn})`),
|
|
56479
58896
|
name: string$3().min(1).describe(`${what} name, for the human approving this action`)
|
|
56480
58897
|
});
|
|
56481
58898
|
/**
|
|
58899
|
+
* A record being created in THIS submission, addressed by the caller's own label.
|
|
58900
|
+
*
|
|
58901
|
+
* It exists because record ids are client-supplied upstream, which is the whole
|
|
58902
|
+
* reason a task can be created into a bucket created alongside it. Rather than ask a
|
|
58903
|
+
* model to mint GUIDs and keep them unique, the caller names the new record and this
|
|
58904
|
+
* file mints the id — returning the mapping in the receipt.
|
|
58905
|
+
*/
|
|
58906
|
+
const pending = strictObject({ ref: string$3().min(1).describe("The `ref` of a record created in this same submission — how a task names a bucket that does not exist yet") });
|
|
58907
|
+
const reference = (what, idColumn) => union$2([existing(what, idColumn), pending]);
|
|
58908
|
+
/** Require a full UTC timestamp because date-only input can land on a different working day. */
|
|
58909
|
+
const instant = datetime$2().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/, "must be a full UTC timestamp like 2026-08-28T17:00:00Z — a bare date is read as midnight and lands on the previous working day");
|
|
58910
|
+
const taskRef = () => reference("task", "msdyn_projecttaskid");
|
|
58911
|
+
const bucketRef = () => reference("bucket", "msdyn_projectbucketid");
|
|
58912
|
+
const taskCreate = strictObject({
|
|
58913
|
+
bucket: bucketRef().describe("The bucket the task goes in"),
|
|
58914
|
+
description: string$3().optional().describe("Body text for the task, shown under its name"),
|
|
58915
|
+
finish: instant.optional().describe("When the task is due"),
|
|
58916
|
+
kind: literal("task"),
|
|
58917
|
+
name: string$3().min(1).describe("The new task's name"),
|
|
58918
|
+
outline_level: number$4().int().min(1).default(1).describe("1 for a top-level task. A subtask is its parent's level plus one — read the parent's msdyn_outlinelevel from the synced tables."),
|
|
58919
|
+
parent_task: taskRef().optional().describe("Makes this a subtask of that task"),
|
|
58920
|
+
priority: number$4().int().min(0).max(10).optional().describe("0-10; 5 is the default"),
|
|
58921
|
+
ref: string$3().min(1).optional().describe("Name this task so other operations here can reference it, and so the receipt returns its id"),
|
|
58922
|
+
start: instant.optional().describe("When work on the task starts")
|
|
58923
|
+
});
|
|
58924
|
+
const bucketCreate = strictObject({
|
|
58925
|
+
kind: literal("bucket"),
|
|
58926
|
+
name: string$3().min(1).describe("The new bucket's name"),
|
|
58927
|
+
ref: string$3().min(1).optional().describe("Name this bucket so tasks in this same submission can go into it")
|
|
58928
|
+
});
|
|
58929
|
+
const taskLabelCreate = strictObject({
|
|
58930
|
+
kind: literal("task_label"),
|
|
58931
|
+
label: existing("label", "msdyn_projectlabelid").describe("The label to put on the task"),
|
|
58932
|
+
task: taskRef().describe("The task getting the label")
|
|
58933
|
+
});
|
|
58934
|
+
const assignmentCreate = strictObject({
|
|
58935
|
+
kind: literal("assignment"),
|
|
58936
|
+
member: existing("project team member", "msdyn_projectteamid").describe("The team member to assign, from msdyn_projectteams"),
|
|
58937
|
+
name: string$3().min(1).describe("A label for the assignment, shown on the approval card"),
|
|
58938
|
+
task: taskRef().describe("The task to assign. A task that has subtasks CANNOT be assigned to — the service refuses it.")
|
|
58939
|
+
});
|
|
58940
|
+
const taskUpdate = strictObject({
|
|
58941
|
+
bucket: bucketRef().optional().describe("Move the task to this bucket, in the same project"),
|
|
58942
|
+
description: string$3().optional(),
|
|
58943
|
+
duration: number$4().optional().describe("Working days. Writing this moves the finish."),
|
|
58944
|
+
effort: number$4().optional().describe("Hours of work"),
|
|
58945
|
+
finish: instant.optional(),
|
|
58946
|
+
kind: literal("task"),
|
|
58947
|
+
name: string$3().min(1).optional().describe("Rename the task to this"),
|
|
58948
|
+
parent_task: taskRef().optional(),
|
|
58949
|
+
priority: number$4().int().min(0).max(10).optional(),
|
|
58950
|
+
progress: number$4().min(0).max(1).optional().describe("Fraction complete, 0-1 — 0.55 is 55%, and 1 is done. NOT a percentage: 55 is rejected."),
|
|
58951
|
+
start: instant.optional(),
|
|
58952
|
+
task: taskRef().describe("The task to change")
|
|
58953
|
+
}).refine((input) => input.bucket !== void 0 || input.description !== void 0 || input.duration !== void 0 || input.effort !== void 0 || input.finish !== void 0 || input.name !== void 0 || input.parent_task !== void 0 || input.priority !== void 0 || input.progress !== void 0 || input.start !== void 0, { message: "Nothing to update — a task update needs at least one changed field." });
|
|
58954
|
+
const bucketUpdate = strictObject({
|
|
58955
|
+
bucket: bucketRef().describe("The bucket to change"),
|
|
58956
|
+
kind: literal("bucket"),
|
|
58957
|
+
name: string$3().min(1).describe("Rename the bucket to this")
|
|
58958
|
+
});
|
|
58959
|
+
const labelUpdate = strictObject({
|
|
58960
|
+
kind: literal("label"),
|
|
58961
|
+
label: existing("label", "msdyn_projectlabelid"),
|
|
58962
|
+
text: string$3().describe("The label's name. A project's labels start unnamed; clearing the text retires one.")
|
|
58963
|
+
});
|
|
58964
|
+
const taskDelete = strictObject({
|
|
58965
|
+
kind: literal("task"),
|
|
58966
|
+
task: existing("task", "msdyn_projecttaskid")
|
|
58967
|
+
});
|
|
58968
|
+
const taskLabelDelete = strictObject({
|
|
58969
|
+
kind: literal("task_label"),
|
|
58970
|
+
link: existing("task-label link", "msdyn_projecttasktolabelid")
|
|
58971
|
+
});
|
|
58972
|
+
const assignmentDelete = strictObject({
|
|
58973
|
+
assignment: existing("assignment", "msdyn_resourceassignmentid"),
|
|
58974
|
+
kind: literal("assignment")
|
|
58975
|
+
});
|
|
58976
|
+
const createOperationSchema = discriminatedUnion("kind", [
|
|
58977
|
+
taskCreate,
|
|
58978
|
+
bucketCreate,
|
|
58979
|
+
taskLabelCreate,
|
|
58980
|
+
assignmentCreate
|
|
58981
|
+
]);
|
|
58982
|
+
const updateOperationSchema = discriminatedUnion("kind", [
|
|
58983
|
+
taskUpdate,
|
|
58984
|
+
bucketUpdate,
|
|
58985
|
+
labelUpdate
|
|
58986
|
+
]);
|
|
58987
|
+
const deleteOperationSchema = discriminatedUnion("kind", [
|
|
58988
|
+
taskDelete,
|
|
58989
|
+
taskLabelDelete,
|
|
58990
|
+
assignmentDelete
|
|
58991
|
+
]);
|
|
58992
|
+
/**
|
|
58993
|
+
* One submission, as the tool takes it.
|
|
58994
|
+
*
|
|
58995
|
+
* Defined at module scope so `Submission` derives from the exact schema and cannot
|
|
58996
|
+
* drift. The refinements enforce cross-field and service-boundary invariants that the
|
|
58997
|
+
* individual field schemas cannot express.
|
|
58998
|
+
*/
|
|
58999
|
+
const submissionSchema = strictObject({
|
|
59000
|
+
creates: array$1(createOperationSchema).default([]).describe("Records to create"),
|
|
59001
|
+
deletes: array$1(deleteOperationSchema).default([]).describe("Records to remove. Buckets cannot be deleted here — removing one takes every task in it."),
|
|
59002
|
+
description: string$3().min(1).describe("What this set does, in the words of the person approving it"),
|
|
59003
|
+
project: existing("project", "msdyn_projectid"),
|
|
59004
|
+
updates: array$1(updateOperationSchema).default([]).describe("Records to change")
|
|
59005
|
+
}).refine((input) => input.creates.length + input.updates.length + input.deletes.length > 0, { message: "Nothing to do — a set needs at least one create, update or delete." }).refine((input) => input.creates.length + input.updates.length + input.deletes.length <= MAX_OPERATIONS, { message: `An operation set carries at most ${MAX_OPERATIONS} operations.` }).refine((input) => [...input.creates, ...input.updates].every((operation) => {
|
|
59006
|
+
const start = "start" in operation ? operation.start : void 0;
|
|
59007
|
+
const finish = "finish" in operation ? operation.finish : void 0;
|
|
59008
|
+
return start === void 0 || finish === void 0 || Date.parse(start) <= Date.parse(finish);
|
|
59009
|
+
}), { message: "finish is before start — the service accepts this, silently recomputes the finish from the duration, and still reports the set completed, so the board would end up holding dates you did not send" });
|
|
59010
|
+
/**
|
|
56482
59011
|
* The caller's Dataverse credentials, recovered entirely from the request: the
|
|
56483
59012
|
* delegated token in `Authorization`, and the org URL from its `aud` claim (`aud` IS
|
|
56484
59013
|
* the `https://<org>.crm.dynamics.com` origin).
|
|
@@ -56499,67 +59028,145 @@ const toolResult = (payload) => ({ content: [{
|
|
|
56499
59028
|
text: JSON.stringify(payload),
|
|
56500
59029
|
type: "text"
|
|
56501
59030
|
}] });
|
|
56502
|
-
|
|
56503
|
-
|
|
59031
|
+
function assertNever(value, where) {
|
|
59032
|
+
throw new Error(`Unhandled ${where}: ${JSON.stringify(value)}`);
|
|
59033
|
+
}
|
|
59034
|
+
/**
|
|
59035
|
+
* Every create paired with its id, and the subset the caller named.
|
|
59036
|
+
*
|
|
59037
|
+
* Mint each create id exactly once before mapping any writes. Client-supplied primary
|
|
59038
|
+
* keys let operations in the same set reference each other; only caller-named refs
|
|
59039
|
+
* enter `createsByRef`.
|
|
59040
|
+
*/
|
|
59041
|
+
function planCreates(requested) {
|
|
59042
|
+
const createsByRef = /* @__PURE__ */ new Map();
|
|
59043
|
+
return {
|
|
59044
|
+
createsByRef,
|
|
59045
|
+
plannedCreates: requested.map((create) => {
|
|
59046
|
+
const id = randomUUID();
|
|
59047
|
+
if ((create.kind === "bucket" || create.kind === "task") && create.ref !== void 0) {
|
|
59048
|
+
if (createsByRef.has(create.ref)) throw new Error(`Two creates in this submission share the ref '${create.ref}' — a ref names exactly one record.`);
|
|
59049
|
+
createsByRef.set(create.ref, {
|
|
59050
|
+
id,
|
|
59051
|
+
kind: create.kind
|
|
59052
|
+
});
|
|
59053
|
+
}
|
|
59054
|
+
return {
|
|
59055
|
+
create,
|
|
59056
|
+
id
|
|
59057
|
+
};
|
|
59058
|
+
})
|
|
59059
|
+
};
|
|
59060
|
+
}
|
|
59061
|
+
function resolveReferenceId(createsByRef, reference, expectedKind) {
|
|
59062
|
+
if ("id" in reference) return reference.id;
|
|
59063
|
+
const created = createsByRef.get(reference.ref);
|
|
59064
|
+
if (created === void 0) throw new Error(`Nothing in this submission is created with the ref '${reference.ref}'. A ref must name a create in the same call; an existing record is named by { id, name }.`);
|
|
59065
|
+
if (created.kind !== expectedKind) throw new Error(`The ref '${reference.ref}' names a ${created.kind}, but this field needs a ${expectedKind}.`);
|
|
59066
|
+
return created.id;
|
|
59067
|
+
}
|
|
59068
|
+
function writesOf(submission, plannedCreates, createsByRef) {
|
|
59069
|
+
const resolveId = (reference, kind) => resolveReferenceId(createsByRef, reference, kind);
|
|
59070
|
+
const projectId = submission.project.id;
|
|
59071
|
+
const staged = [];
|
|
59072
|
+
for (const { create, id: newId } of plannedCreates) if (create.kind === "task") staged.push(createProjectTask({
|
|
59073
|
+
bucketId: resolveId(create.bucket, "bucket"),
|
|
59074
|
+
description: create.description,
|
|
59075
|
+
finish: create.finish,
|
|
59076
|
+
outlineLevel: create.outline_level,
|
|
59077
|
+
parentTaskId: create.parent_task === void 0 ? void 0 : resolveId(create.parent_task, "task"),
|
|
59078
|
+
priority: create.priority,
|
|
59079
|
+
projectId,
|
|
59080
|
+
start: create.start,
|
|
59081
|
+
subject: create.name,
|
|
59082
|
+
taskId: newId
|
|
59083
|
+
}));
|
|
59084
|
+
else if (create.kind === "bucket") staged.push(createProjectBucket({
|
|
59085
|
+
bucketId: newId,
|
|
59086
|
+
name: create.name,
|
|
59087
|
+
projectId
|
|
59088
|
+
}));
|
|
59089
|
+
else if (create.kind === "task_label") staged.push(createTaskLabel({
|
|
59090
|
+
labelId: create.label.id,
|
|
59091
|
+
linkId: newId,
|
|
59092
|
+
taskId: resolveId(create.task, "task")
|
|
59093
|
+
}));
|
|
59094
|
+
else if (create.kind === "assignment") staged.push(createResourceAssignment({
|
|
59095
|
+
assignmentId: newId,
|
|
59096
|
+
name: create.name,
|
|
59097
|
+
projectId,
|
|
59098
|
+
projectTeamId: create.member.id,
|
|
59099
|
+
taskId: resolveId(create.task, "task")
|
|
59100
|
+
}));
|
|
59101
|
+
else assertNever(create, "create operation");
|
|
59102
|
+
for (const update of submission.updates) if (update.kind === "task") staged.push(updateProjectTask({
|
|
59103
|
+
bucketId: update.bucket === void 0 ? void 0 : resolveId(update.bucket, "bucket"),
|
|
59104
|
+
description: update.description,
|
|
59105
|
+
duration: update.duration,
|
|
59106
|
+
effort: update.effort,
|
|
59107
|
+
finish: update.finish,
|
|
59108
|
+
parentTaskId: update.parent_task === void 0 ? void 0 : resolveId(update.parent_task, "task"),
|
|
59109
|
+
priority: update.priority,
|
|
59110
|
+
progress: update.progress,
|
|
59111
|
+
start: update.start,
|
|
59112
|
+
subject: update.name,
|
|
59113
|
+
taskId: resolveId(update.task, "task")
|
|
59114
|
+
}));
|
|
59115
|
+
else if (update.kind === "bucket") staged.push(updateProjectBucket({
|
|
59116
|
+
bucketId: resolveId(update.bucket, "bucket"),
|
|
59117
|
+
name: update.name
|
|
59118
|
+
}));
|
|
59119
|
+
else if (update.kind === "label") staged.push(updateProjectLabel({
|
|
59120
|
+
labelId: update.label.id,
|
|
59121
|
+
text: update.text
|
|
59122
|
+
}));
|
|
59123
|
+
else assertNever(update, "update operation");
|
|
59124
|
+
for (const deleteOperation of submission.deletes) if (deleteOperation.kind === "task") staged.push(deleteRecord({
|
|
59125
|
+
id: deleteOperation.task.id,
|
|
59126
|
+
kind: "task"
|
|
59127
|
+
}));
|
|
59128
|
+
else if (deleteOperation.kind === "task_label") staged.push(deleteRecord({
|
|
59129
|
+
id: deleteOperation.link.id,
|
|
59130
|
+
kind: "taskLabel"
|
|
59131
|
+
}));
|
|
59132
|
+
else if (deleteOperation.kind === "assignment") staged.push(deleteRecord({
|
|
59133
|
+
id: deleteOperation.assignment.id,
|
|
59134
|
+
kind: "assignment"
|
|
59135
|
+
}));
|
|
59136
|
+
else assertNever(deleteOperation, "delete operation");
|
|
59137
|
+
return staged;
|
|
59138
|
+
}
|
|
56504
59139
|
function createMcpServer() {
|
|
56505
59140
|
const server = new McpServer({
|
|
56506
59141
|
name: "microsoft-planner-premium",
|
|
56507
59142
|
version: SERVER_VERSION
|
|
56508
59143
|
});
|
|
56509
|
-
server.registerTool("
|
|
56510
|
-
description:
|
|
56511
|
-
|
|
56512
|
-
bucket
|
|
56513
|
-
name
|
|
56514
|
-
|
|
56515
|
-
|
|
56516
|
-
|
|
56517
|
-
|
|
59144
|
+
server.registerTool("submit_operation_set", {
|
|
59145
|
+
description: [
|
|
59146
|
+
"Change a Premium Planner (Project for the web) board. One call is one OPERATION SET — the scheduling service's own unit of work: a transaction over many operations, all against ONE project, applied together or not at all.",
|
|
59147
|
+
"Put everything one intent needs into a single call. Creating a bucket and the tasks that go in it, or renaming a task and setting its dates and assigning it, is one set and one approval — not several. A task can go into a bucket created in the SAME call by giving that bucket a `ref` and naming it.",
|
|
59148
|
+
"Read every id from the synced msdyn_* tables; each id travels with the matching name, which the human sees in the approval arguments. `description` is yours to write, so summarize the set in that human's words.",
|
|
59149
|
+
"Changing a task's dates can move OTHER tasks: tasks are auto-scheduled, so a task that has dependents drags them along, and the set reports only that it completed.",
|
|
59150
|
+
"Returns an operation_set_id — a RECEIPT, not a confirmation: the set has been accepted, not applied. Pass it to wait_for_operation_set to learn whether it landed."
|
|
59151
|
+
].join(" "),
|
|
59152
|
+
inputSchema: submissionSchema
|
|
59153
|
+
}, async (submission, extra) => {
|
|
59154
|
+
const { createsByRef, plannedCreates } = planCreates(submission.creates);
|
|
59155
|
+
const operationSetId = await submitOperationSet(scheduleApiFromRequest(extra.requestInfo), {
|
|
59156
|
+
description: submission.description,
|
|
59157
|
+
projectId: submission.project.id,
|
|
59158
|
+
writes: writesOf(submission, plannedCreates, createsByRef)
|
|
59159
|
+
});
|
|
56518
59160
|
return toolResult({
|
|
56519
|
-
|
|
56520
|
-
|
|
56521
|
-
projectId: project.id,
|
|
56522
|
-
write: createProjectTask({
|
|
56523
|
-
bucketId: bucket.id,
|
|
56524
|
-
projectId: project.id,
|
|
56525
|
-
subject: name,
|
|
56526
|
-
taskId
|
|
56527
|
-
})
|
|
56528
|
-
}),
|
|
56529
|
-
task_id: taskId
|
|
59161
|
+
created: Object.fromEntries([...createsByRef].map(([ref, created]) => [ref, created.id])),
|
|
59162
|
+
operation_set_id: operationSetId
|
|
56530
59163
|
});
|
|
56531
59164
|
});
|
|
56532
|
-
server.registerTool("update_task", {
|
|
56533
|
-
description: `Change an existing task on a Premium Planner (Project for the web) board — rename it, move it to another bucket, set how complete it is, or any combination, applied as one atomic write. Read the ids from the synced msdyn_* tables; each id travels with the matching name, which is what the human approving this action reads. ${SAME_PROJECT} ${RECEIPT}`,
|
|
56534
|
-
inputSchema: strictObject({
|
|
56535
|
-
changes: strictObject({
|
|
56536
|
-
bucket: reference("bucket", "msdyn_projectbucketid").optional().describe("Move the task to this bucket, in the same project"),
|
|
56537
|
-
name: string$3().min(1).optional().describe("Rename the task to this"),
|
|
56538
|
-
progress: number$4().min(0).max(1).optional().describe("Fraction complete, 0-1 — 0.55 is 55%, and 1 is done. NOT a percentage: 55 is rejected.")
|
|
56539
|
-
}).describe("What to change about the task — at least one of these."),
|
|
56540
|
-
project: reference("project", "msdyn_projectid"),
|
|
56541
|
-
task: reference("task", "msdyn_projecttaskid")
|
|
56542
|
-
}).refine((input) => Object.keys(input.changes).length > 0, {
|
|
56543
|
-
message: "Nothing to update — changes needs at least one of name, bucket, progress.",
|
|
56544
|
-
path: ["changes"]
|
|
56545
|
-
})
|
|
56546
|
-
}, async ({ changes, project, task }, extra) => {
|
|
56547
|
-
return toolResult({ operation_set_id: await submitWrite(scheduleApiFromRequest(extra.requestInfo), {
|
|
56548
|
-
description: `ContextLayer update_task: ${task.name}`,
|
|
56549
|
-
projectId: project.id,
|
|
56550
|
-
write: updateProjectTask({
|
|
56551
|
-
bucketId: changes.bucket?.id,
|
|
56552
|
-
progress: changes.progress,
|
|
56553
|
-
subject: changes.name,
|
|
56554
|
-
taskId: task.id
|
|
56555
|
-
})
|
|
56556
|
-
}) });
|
|
56557
|
-
});
|
|
56558
59165
|
server.registerTool("wait_for_operation_set", {
|
|
56559
|
-
description: "Find out what became of a
|
|
59166
|
+
description: "Find out what became of a submitted set. Takes the operation_set_id returned by submit_operation_set and reads it once. While the status is executing, it polls until the set becomes terminal or the window closes; any other status returns immediately. Reports the scheduling service's own status: completed, failed (with its reason), executing, open, or abandoned. Note that 'completed' means the service applied the set without error — it is not a promise that a field holds the value you sent.",
|
|
56560
59167
|
inputSchema: strictObject({
|
|
56561
|
-
operation_set_id: string$3().min(1).describe("The operation_set_id returned by
|
|
56562
|
-
seconds: number$4().int().min(0).max(45).describe(`How long to wait, 0-45. 0 reads once and returns whatever
|
|
59168
|
+
operation_set_id: string$3().min(1).describe("The operation_set_id returned by submit_operation_set"),
|
|
59169
|
+
seconds: number$4().int().min(0).max(45).describe(`How long to wait, 0-45. 0 reads once and returns whatever the service says. If it still reports 'executing' when the window closes, call again.`)
|
|
56563
59170
|
})
|
|
56564
59171
|
}, async ({ operation_set_id, seconds }, extra) => {
|
|
56565
59172
|
return toolResult({ ...await waitForOperationSet(scheduleApiFromRequest(extra.requestInfo), {
|
|
@@ -83896,7 +86503,7 @@ function osUsername() {
|
|
|
83896
86503
|
}
|
|
83897
86504
|
|
|
83898
86505
|
//#endregion
|
|
83899
|
-
//#region ../slate-bridge/dist/goldens-
|
|
86506
|
+
//#region ../slate-bridge/dist/goldens-CEuJ0Yli.mjs
|
|
83900
86507
|
const nonEmptyStringSchema = string$3().trim().min(1);
|
|
83901
86508
|
const portSchema = number$3().int().min(0).max(65535).default(7777);
|
|
83902
86509
|
const envSchema = object$4({
|
|
@@ -83905,7 +86512,7 @@ const envSchema = object$4({
|
|
|
83905
86512
|
CTX_PLATFORM_URL: url().default("http://127.0.0.1:3010").transform((url) => url.replace(/\/+$/, "")),
|
|
83906
86513
|
CTX_WEB_URL: url().default("http://localhost:3000").transform((url) => url.replace(/\/+$/, "")),
|
|
83907
86514
|
CTXS_BRIDGE_PORT: portSchema,
|
|
83908
|
-
CTXS_CLAUDE_IMAGE: nonEmptyStringSchema.default("ghcr.io/usecontextlayer/ctx-sandbox:0.5.
|
|
86515
|
+
CTXS_CLAUDE_IMAGE: nonEmptyStringSchema.default("ghcr.io/usecontextlayer/ctx-sandbox:0.5.16"),
|
|
83909
86516
|
CTXS_CLAUDE_MODEL: nonEmptyStringSchema.default("opus"),
|
|
83910
86517
|
CTXS_HOST_CLAUDE_JSON_PATH: nonEmptyStringSchema.default(() => path.join(os.homedir(), ".claude.json")),
|
|
83911
86518
|
NODE_ENV: _enum$1([
|
|
@@ -83942,6 +86549,30 @@ You are working in the user's workspace: a directory of documents that is also a
|
|
|
83942
86549
|
Do this bookkeeping quietly. Don't narrate routine git operations — mention git only when something needs the user's attention: a conflict, a rejected push you cannot heal, or work you found and preserved.
|
|
83943
86550
|
|
|
83944
86551
|
The workspace's synced source data — email, meetings, tasks, whatever the user connected — lives in a Postgres database called the base. Query it with the \`ctxb\` CLI, following the contextbase skill. \`CTXB_DATABASE_URL\` is already set when this workspace has a base; if it is unset, no base is connected. Answer data questions from the base, not from memory.`;
|
|
86552
|
+
const BUILTIN_ALLOWED_TOOLS = [
|
|
86553
|
+
"Agent",
|
|
86554
|
+
"AskUserQuestion",
|
|
86555
|
+
"Bash",
|
|
86556
|
+
"BashOutput",
|
|
86557
|
+
"Edit",
|
|
86558
|
+
"EnterPlanMode",
|
|
86559
|
+
"ExitPlanMode",
|
|
86560
|
+
"Glob",
|
|
86561
|
+
"Grep",
|
|
86562
|
+
"KillShell",
|
|
86563
|
+
"ListMcpResources",
|
|
86564
|
+
"MultiEdit",
|
|
86565
|
+
"NotebookEdit",
|
|
86566
|
+
"Read",
|
|
86567
|
+
"ReadMcpResource",
|
|
86568
|
+
"Skill",
|
|
86569
|
+
"SlashCommand",
|
|
86570
|
+
"Task",
|
|
86571
|
+
"TodoWrite",
|
|
86572
|
+
"WebFetch",
|
|
86573
|
+
"WebSearch",
|
|
86574
|
+
"Write"
|
|
86575
|
+
];
|
|
83945
86576
|
const CLAUDE_ARGS = [
|
|
83946
86577
|
"--input-format",
|
|
83947
86578
|
"stream-json",
|
|
@@ -83955,31 +86586,6 @@ const CLAUDE_ARGS = [
|
|
|
83955
86586
|
"summarized",
|
|
83956
86587
|
"--permission-mode",
|
|
83957
86588
|
"default",
|
|
83958
|
-
"--allowedTools",
|
|
83959
|
-
[
|
|
83960
|
-
"Agent",
|
|
83961
|
-
"AskUserQuestion",
|
|
83962
|
-
"Bash",
|
|
83963
|
-
"BashOutput",
|
|
83964
|
-
"Edit",
|
|
83965
|
-
"EnterPlanMode",
|
|
83966
|
-
"ExitPlanMode",
|
|
83967
|
-
"Glob",
|
|
83968
|
-
"Grep",
|
|
83969
|
-
"KillShell",
|
|
83970
|
-
"ListMcpResources",
|
|
83971
|
-
"MultiEdit",
|
|
83972
|
-
"NotebookEdit",
|
|
83973
|
-
"Read",
|
|
83974
|
-
"ReadMcpResource",
|
|
83975
|
-
"Skill",
|
|
83976
|
-
"SlashCommand",
|
|
83977
|
-
"Task",
|
|
83978
|
-
"TodoWrite",
|
|
83979
|
-
"WebFetch",
|
|
83980
|
-
"WebSearch",
|
|
83981
|
-
"Write"
|
|
83982
|
-
].join(","),
|
|
83983
86589
|
"--permission-prompt-tool",
|
|
83984
86590
|
"stdio",
|
|
83985
86591
|
"--append-system-prompt-file",
|
|
@@ -83991,9 +86597,13 @@ function resolveChatBaseId(baseIds, workspaceId) {
|
|
|
83991
86597
|
return baseId;
|
|
83992
86598
|
}
|
|
83993
86599
|
function buildChatSpawnPlan(input) {
|
|
86600
|
+
const { allowedTools, disallowedTools } = input.mcpToolRules;
|
|
83994
86601
|
return {
|
|
83995
86602
|
args: [
|
|
83996
86603
|
...CLAUDE_ARGS,
|
|
86604
|
+
"--allowedTools",
|
|
86605
|
+
[...BUILTIN_ALLOWED_TOOLS, ...allowedTools].join(","),
|
|
86606
|
+
...disallowedTools.length > 0 ? ["--disallowedTools", disallowedTools.join(",")] : [],
|
|
83997
86607
|
...input.mcpConfigArgs ?? [],
|
|
83998
86608
|
"--model",
|
|
83999
86609
|
input.model,
|
|
@@ -84007,6 +86617,47 @@ function buildChatSpawnPlan(input) {
|
|
|
84007
86617
|
}
|
|
84008
86618
|
};
|
|
84009
86619
|
}
|
|
86620
|
+
/** Wait for every promise before propagating the first rejection in input order. */
|
|
86621
|
+
async function joinAll(promises) {
|
|
86622
|
+
const failed = (await Promise.allSettled(promises)).find((result) => result.status === "rejected");
|
|
86623
|
+
if (failed !== void 0) throw failed.reason;
|
|
86624
|
+
}
|
|
86625
|
+
function liveChatKey(repoId, sessionId) {
|
|
86626
|
+
return `${repoId}\n${sessionId}`;
|
|
86627
|
+
}
|
|
86628
|
+
/**
|
|
86629
|
+
* Delete one chat session: stop whatever is live on it, then drop its transcript from the
|
|
86630
|
+
* claude home and snapshot the removal. Returns false when the home holds no such
|
|
86631
|
+
* transcript — a chat the user opened but never sent a turn in, which is a legitimate
|
|
86632
|
+
* no-op rather than a failure. THROWS when the home cannot be made local or the removal
|
|
86633
|
+
* cannot be pushed; a rejection means remote deletion was not proven, and the caller must
|
|
86634
|
+
* not report success.
|
|
86635
|
+
*
|
|
86636
|
+
* THE ORDER IS THE WHOLE FUNCTION, and every step is here because skipping it makes the
|
|
86637
|
+
* delete silently undo itself:
|
|
86638
|
+
* 1. `stop()` every live connection on this chat — request the terminal client signal and
|
|
86639
|
+
* abort its claude work. A connection deleted before its socket opens delivers the signal
|
|
86640
|
+
* on open.
|
|
86641
|
+
* 2. join every `done` — a connection that reached a turn may fire a final `capture()`
|
|
86642
|
+
* after disposal. Remove the file before that lands and the capture pushes it straight
|
|
86643
|
+
* back; worse, a guest still running re-creates the transcript outright. The settled
|
|
86644
|
+
* join waits for every writer before propagating a teardown failure.
|
|
86645
|
+
* 3. `restore()` — the delete may be this process's first contact with the home. The
|
|
86646
|
+
* sidebar lists chats from pggit, not from the host dir, so deleting a chat you never
|
|
86647
|
+
* opened is the NORMAL path; without this the removal finds an empty dir, reports
|
|
86648
|
+
* "nothing here", and the chat survives. After the join, so the clone never lands on a
|
|
86649
|
+
* dir a guest is writing.
|
|
86650
|
+
* 4. `settle()` — drain the capture chain, so nothing already queued pushes after the rm.
|
|
86651
|
+
* 5. `removeTranscript` — rm + capture, the git half. Steps 1-4 are exactly the
|
|
86652
|
+
* precondition it documents and cannot check for itself.
|
|
86653
|
+
*/
|
|
86654
|
+
async function deleteChatSession(input) {
|
|
86655
|
+
for (const liveChatSession of input.liveChatSessions) liveChatSession.stop();
|
|
86656
|
+
await joinAll(input.liveChatSessions.map((liveChatSession) => liveChatSession.done));
|
|
86657
|
+
await input.claudeRepo.restore();
|
|
86658
|
+
await input.claudeRepo.settle();
|
|
86659
|
+
return input.claudeRepo.removeTranscript(input.sessionId);
|
|
86660
|
+
}
|
|
84010
86661
|
function createProviderTokenClient(config) {
|
|
84011
86662
|
const fetchImpl = config.fetchImpl ?? fetch;
|
|
84012
86663
|
const redeemUrl = `${config.webUrl}/api/auth/provider-token`;
|
|
@@ -84053,17 +86704,57 @@ async function resolveMcpMounts(input) {
|
|
|
84053
86704
|
}
|
|
84054
86705
|
return mounts;
|
|
84055
86706
|
}
|
|
86707
|
+
function sanitizeNameSegment(segment) {
|
|
86708
|
+
return segment.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
86709
|
+
}
|
|
86710
|
+
function mcpToolRuleName(serverName, wireToolName) {
|
|
86711
|
+
return `mcp__${sanitizeNameSegment(serverName)}__${sanitizeNameSegment(wireToolName)}`;
|
|
86712
|
+
}
|
|
86713
|
+
function compileToolRules(input) {
|
|
86714
|
+
const declared = /* @__PURE__ */ new Set();
|
|
86715
|
+
const allowedTools = [];
|
|
86716
|
+
for (const [tool, config] of Object.entries(input.enabledTools)) {
|
|
86717
|
+
declared.add(tool);
|
|
86718
|
+
if (config.autoAllow) allowedTools.push(mcpToolRuleName(input.serverName, tool));
|
|
86719
|
+
}
|
|
86720
|
+
const missing = new Set(declared);
|
|
86721
|
+
const disallowedTools = [];
|
|
86722
|
+
for (const tool of input.census) {
|
|
86723
|
+
missing.delete(tool);
|
|
86724
|
+
if (!declared.has(tool)) disallowedTools.push(mcpToolRuleName(input.serverName, tool));
|
|
86725
|
+
}
|
|
86726
|
+
if (missing.size > 0) throw new Error(`mcp server '${input.serverName}': enabledTools name(s) the server does not expose: ${[...missing].join(", ")} — the manifest is stale against the live census.`);
|
|
86727
|
+
return {
|
|
86728
|
+
allowedTools,
|
|
86729
|
+
disallowedTools
|
|
86730
|
+
};
|
|
86731
|
+
}
|
|
84056
86732
|
function isServableMount(mount) {
|
|
84057
86733
|
return mount.bindingAuth.type === "authenticated_account";
|
|
84058
86734
|
}
|
|
84059
86735
|
function unknownModuleError(mount, moduleName) {
|
|
84060
86736
|
return /* @__PURE__ */ new Error(`MCP server '${mount.serverName}' names module '${moduleName}', which this bridge build does not carry.`);
|
|
84061
86737
|
}
|
|
86738
|
+
async function listToolCensus(transport) {
|
|
86739
|
+
const client = new Client({
|
|
86740
|
+
name: "slate-bridge-census",
|
|
86741
|
+
version: "0.0.0"
|
|
86742
|
+
});
|
|
86743
|
+
await client.connect(transport);
|
|
86744
|
+
try {
|
|
86745
|
+
const page = await client.listTools();
|
|
86746
|
+
if (page.nextCursor !== void 0) throw new Error("mcp census: server paginates tools/list; census pagination is not built");
|
|
86747
|
+
return page.tools.map((tool) => tool.name);
|
|
86748
|
+
} finally {
|
|
86749
|
+
await client.close();
|
|
86750
|
+
}
|
|
86751
|
+
}
|
|
84062
86752
|
function createMcpGateway(input) {
|
|
84063
86753
|
const sessions = /* @__PURE__ */ new Map();
|
|
84064
86754
|
const providerToken = createProviderTokenClient({ webUrl: input.webUrl });
|
|
86755
|
+
const moduleFactories = new Map(Object.entries(input.moduleServers));
|
|
84065
86756
|
const moduleMounts = /* @__PURE__ */ new Map();
|
|
84066
|
-
for (const [moduleName, createServer] of
|
|
86757
|
+
for (const [moduleName, createServer] of moduleFactories) {
|
|
84067
86758
|
const moduleApp = new Hono();
|
|
84068
86759
|
moduleApp.all("*", async (c) => {
|
|
84069
86760
|
const transport = new StreamableHTTPTransport();
|
|
@@ -84142,6 +86833,49 @@ function createMcpGateway(input) {
|
|
|
84142
86833
|
registerSession: (sessionToken, session) => {
|
|
84143
86834
|
sessions.set(sessionToken, session);
|
|
84144
86835
|
},
|
|
86836
|
+
resolveGuestMcpPlan: async (mounts, session) => {
|
|
86837
|
+
const serverNames = [];
|
|
86838
|
+
const allowedTools = [];
|
|
86839
|
+
const disallowedTools = [];
|
|
86840
|
+
for (const mount of mounts) {
|
|
86841
|
+
let census;
|
|
86842
|
+
if (mount.server.type === "http") {
|
|
86843
|
+
const lease = await providerToken.redeem({
|
|
86844
|
+
accountId: mount.bindingAuth.accountId,
|
|
86845
|
+
providerId: mount.bindingAuth.providerId,
|
|
86846
|
+
refresh: false,
|
|
86847
|
+
userToken: session.userToken
|
|
86848
|
+
});
|
|
86849
|
+
const transport = new StreamableHTTPClientTransport(new URL(mount.server.url), { requestInit: { headers: { authorization: `Bearer ${lease.accessToken}` } } });
|
|
86850
|
+
try {
|
|
86851
|
+
census = await listToolCensus(transport);
|
|
86852
|
+
} catch (error) {
|
|
86853
|
+
throw new Error(`census for mcp server '${mount.serverName}' failed against ${mount.server.url}`, { cause: error });
|
|
86854
|
+
}
|
|
86855
|
+
} else {
|
|
86856
|
+
const factory = moduleFactories.get(mount.server.module);
|
|
86857
|
+
if (factory === void 0) throw unknownModuleError(mount, mount.server.module);
|
|
86858
|
+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
|
|
86859
|
+
await factory().connect(serverTransport);
|
|
86860
|
+
census = await listToolCensus(clientTransport);
|
|
86861
|
+
}
|
|
86862
|
+
const rules = compileToolRules({
|
|
86863
|
+
census,
|
|
86864
|
+
enabledTools: mount.server.enabledTools,
|
|
86865
|
+
serverName: mount.serverName
|
|
86866
|
+
});
|
|
86867
|
+
serverNames.push(mount.serverName);
|
|
86868
|
+
allowedTools.push(...rules.allowedTools);
|
|
86869
|
+
disallowedTools.push(...rules.disallowedTools);
|
|
86870
|
+
}
|
|
86871
|
+
return {
|
|
86872
|
+
serverNames,
|
|
86873
|
+
toolRules: {
|
|
86874
|
+
allowedTools,
|
|
86875
|
+
disallowedTools
|
|
86876
|
+
}
|
|
86877
|
+
};
|
|
86878
|
+
},
|
|
84145
86879
|
selectServableMounts: selectServable
|
|
84146
86880
|
};
|
|
84147
86881
|
}
|
|
@@ -84157,6 +86891,7 @@ function registerMcpGatewayRoutes(app, gateway) {
|
|
|
84157
86891
|
function buildMcpConfigArgs(input) {
|
|
84158
86892
|
if (input.serverNames.length === 0) return [];
|
|
84159
86893
|
const mcpServers = Object.fromEntries(input.serverNames.map((serverName) => [serverName, {
|
|
86894
|
+
alwaysLoad: true,
|
|
84160
86895
|
headers: { Authorization: `Bearer ${input.sessionToken}` },
|
|
84161
86896
|
type: "http",
|
|
84162
86897
|
url: rewriteHostUrlForGuest(`${input.bridgeOrigin}/bridge/mcp/${serverName}`)
|
|
@@ -84196,17 +86931,14 @@ async function createClaudeMicrosandbox(options) {
|
|
|
84196
86931
|
});
|
|
84197
86932
|
await ensureHostFilesystemReady(bootConfig.bindMounts);
|
|
84198
86933
|
const { guestMemory, sandbox } = await bootSandbox(await import("microsandbox"), bootConfig);
|
|
84199
|
-
let
|
|
86934
|
+
let disposePromise = null;
|
|
84200
86935
|
return {
|
|
84201
|
-
dispose:
|
|
84202
|
-
if (
|
|
84203
|
-
|
|
84204
|
-
|
|
84205
|
-
try {
|
|
84206
|
-
await sandbox.stopWithTimeout(SANDBOX_STOP_TIMEOUT_MS);
|
|
84207
|
-
} catch (error) {
|
|
84208
|
-
console.error("[slate-bridge] sandbox stop failed:", error);
|
|
86936
|
+
dispose: () => {
|
|
86937
|
+
if (disposePromise === null) {
|
|
86938
|
+
guestMemory.stop();
|
|
86939
|
+
disposePromise = sandbox.stopWithTimeout(SANDBOX_STOP_TIMEOUT_MS);
|
|
84209
86940
|
}
|
|
86941
|
+
return disposePromise;
|
|
84210
86942
|
},
|
|
84211
86943
|
guestMemory: () => guestMemory.read(),
|
|
84212
86944
|
spawn: (spawnOptions) => spawnClaudeProcess(sandbox, spawnOptions)
|
|
@@ -84506,6 +87238,31 @@ var WorkspaceDocsCache = class {
|
|
|
84506
87238
|
}
|
|
84507
87239
|
};
|
|
84508
87240
|
(0, import_undici.setGlobalDispatcher)(new import_undici.Agent({ allowH2: false }));
|
|
87241
|
+
const CHAT_HOME_CONFIG = {
|
|
87242
|
+
maintainIndex: true,
|
|
87243
|
+
restore: true,
|
|
87244
|
+
watch: HOME_WATCH_CONFIG
|
|
87245
|
+
};
|
|
87246
|
+
const CHAT_SESSION_PATH_SCHEMA = strictObject({
|
|
87247
|
+
sessionId: string$3().uuid(),
|
|
87248
|
+
workspaceId: string$3().uuid()
|
|
87249
|
+
});
|
|
87250
|
+
async function resolveClaudeHomeRepo(input) {
|
|
87251
|
+
const repoId = buildRepoId({
|
|
87252
|
+
kind: "claude",
|
|
87253
|
+
owner: "slate",
|
|
87254
|
+
userId: input.sub,
|
|
87255
|
+
workspaceId: input.workspaceId
|
|
87256
|
+
});
|
|
87257
|
+
return {
|
|
87258
|
+
claudeRepo: ClaudeRepo.get(await resolveRepo({
|
|
87259
|
+
platformUrl: input.platformUrl,
|
|
87260
|
+
repoId,
|
|
87261
|
+
token: input.token
|
|
87262
|
+
}, input.gitHostBaseDir), CHAT_HOME_CONFIG),
|
|
87263
|
+
repoId
|
|
87264
|
+
};
|
|
87265
|
+
}
|
|
84509
87266
|
function bearerToken(c) {
|
|
84510
87267
|
const header = c.req.header("Authorization") ?? "";
|
|
84511
87268
|
return header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
@@ -84543,7 +87300,7 @@ function wsReject(code, reason) {
|
|
|
84543
87300
|
wsCtx.close(code, reason);
|
|
84544
87301
|
} };
|
|
84545
87302
|
}
|
|
84546
|
-
function
|
|
87303
|
+
function wsCloseWithErrorFrame(ws, code, reason) {
|
|
84547
87304
|
if (ws === null || ws.readyState !== WebSocket.OPEN) return;
|
|
84548
87305
|
ws.send(`${JSON.stringify({
|
|
84549
87306
|
reason,
|
|
@@ -84573,6 +87330,8 @@ async function createBridgeServer(opts) {
|
|
|
84573
87330
|
});
|
|
84574
87331
|
let boundPort = 0;
|
|
84575
87332
|
const inflight = /* @__PURE__ */ new Set();
|
|
87333
|
+
const liveChatSessions = /* @__PURE__ */ new Set();
|
|
87334
|
+
const chatDeletionPromises = /* @__PURE__ */ new Map();
|
|
84576
87335
|
const docsCache = new WorkspaceDocsCache();
|
|
84577
87336
|
const app = new Hono();
|
|
84578
87337
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -84628,32 +87387,71 @@ async function createBridgeServer(opts) {
|
|
|
84628
87387
|
return c.text(err instanceof Error ? err.message : String(err), 400);
|
|
84629
87388
|
}
|
|
84630
87389
|
});
|
|
87390
|
+
app.delete("/bridge/workspaces/:workspaceId/sessions/:id", renderAuth, async (c) => {
|
|
87391
|
+
const sub = c.get("sub");
|
|
87392
|
+
if (!sub) return c.text("chat sessions are user-scoped", 403);
|
|
87393
|
+
const parsedPath = CHAT_SESSION_PATH_SCHEMA.safeParse({
|
|
87394
|
+
sessionId: c.req.param("id"),
|
|
87395
|
+
workspaceId: c.req.param("workspaceId")
|
|
87396
|
+
});
|
|
87397
|
+
if (!parsedPath.success) return c.text(prettifyError(parsedPath.error), 400);
|
|
87398
|
+
const { sessionId, workspaceId } = parsedPath.data;
|
|
87399
|
+
try {
|
|
87400
|
+
const { claudeRepo, repoId } = await resolveClaudeHomeRepo({
|
|
87401
|
+
gitHostBaseDir,
|
|
87402
|
+
platformUrl,
|
|
87403
|
+
sub,
|
|
87404
|
+
token: bearerToken(c),
|
|
87405
|
+
workspaceId
|
|
87406
|
+
});
|
|
87407
|
+
const key = liveChatKey(repoId, sessionId);
|
|
87408
|
+
let deletionPromise = chatDeletionPromises.get(key);
|
|
87409
|
+
if (deletionPromise === void 0) {
|
|
87410
|
+
deletionPromise = deleteChatSession({
|
|
87411
|
+
claudeRepo,
|
|
87412
|
+
liveChatSessions: [...liveChatSessions].filter((liveChatSession) => liveChatSession.key === key),
|
|
87413
|
+
sessionId
|
|
87414
|
+
});
|
|
87415
|
+
chatDeletionPromises.set(key, deletionPromise);
|
|
87416
|
+
deletionPromise.then(() => chatDeletionPromises.delete(key), () => chatDeletionPromises.delete(key));
|
|
87417
|
+
}
|
|
87418
|
+
await deletionPromise;
|
|
87419
|
+
return c.body(null, 204);
|
|
87420
|
+
} catch (err) {
|
|
87421
|
+
console.error(`[slate-bridge] chat delete failed (${sessionId}):`, err);
|
|
87422
|
+
Sentry.captureException(err, { extra: {
|
|
87423
|
+
sessionId,
|
|
87424
|
+
workspaceId
|
|
87425
|
+
} });
|
|
87426
|
+
return c.text(err instanceof Error ? err.message : String(err), 500);
|
|
87427
|
+
}
|
|
87428
|
+
});
|
|
84631
87429
|
app.get("/bridge/workspaces/:workspaceId/sessions/:id/wss", upgradeWebSocket(async (c) => {
|
|
84632
|
-
const workspaceId = c.req.param("workspaceId") ?? "";
|
|
84633
|
-
const sessionId = c.req.param("id") ?? "";
|
|
84634
87430
|
const userToken = c.req.query("token") ?? "";
|
|
84635
87431
|
const r = await verify(userToken);
|
|
84636
87432
|
if (!r.ok) return wsReject(4401, "unauthorized");
|
|
84637
87433
|
if (r.kind !== "user") return wsReject(4403, "forbidden: machine token");
|
|
84638
|
-
|
|
84639
|
-
|
|
84640
|
-
|
|
84641
|
-
|
|
84642
|
-
workspaceId
|
|
87434
|
+
if (!r.sub) return wsReject(4403, "forbidden: empty user identity");
|
|
87435
|
+
const parsedPath = CHAT_SESSION_PATH_SCHEMA.safeParse({
|
|
87436
|
+
sessionId: c.req.param("id"),
|
|
87437
|
+
workspaceId: c.req.param("workspaceId")
|
|
84643
87438
|
});
|
|
84644
|
-
|
|
87439
|
+
if (!parsedPath.success) return wsReject(4400, "invalid chat session path");
|
|
87440
|
+
const { sessionId, workspaceId } = parsedPath.data;
|
|
87441
|
+
const { claudeRepo, repoId } = await resolveClaudeHomeRepo({
|
|
87442
|
+
gitHostBaseDir,
|
|
84645
87443
|
platformUrl,
|
|
84646
|
-
|
|
84647
|
-
token: userToken
|
|
84648
|
-
|
|
84649
|
-
maintainIndex: true,
|
|
84650
|
-
restore: true,
|
|
84651
|
-
watch: HOME_WATCH_CONFIG
|
|
87444
|
+
sub: r.sub,
|
|
87445
|
+
token: userToken,
|
|
87446
|
+
workspaceId
|
|
84652
87447
|
});
|
|
87448
|
+
const key = liveChatKey(repoId, sessionId);
|
|
87449
|
+
if (chatDeletionPromises.has(key)) return wsReject(4410, "session deletion in progress");
|
|
84653
87450
|
if (!claudeRepo.tryAcquireSession(sessionId)) return wsReject(4409, "session already in use by another connection");
|
|
84654
87451
|
const earlyBuffer = [];
|
|
84655
87452
|
let stdin = null;
|
|
84656
87453
|
let ws = null;
|
|
87454
|
+
let deletionRequested = false;
|
|
84657
87455
|
const ac = new AbortController();
|
|
84658
87456
|
const work = (async () => {
|
|
84659
87457
|
console.log("[slate-bridge] connection opened");
|
|
@@ -84684,12 +87482,16 @@ async function createBridgeServer(opts) {
|
|
|
84684
87482
|
});
|
|
84685
87483
|
const { servable: servableMounts, skipped: skippedMounts } = mcpGateway.selectServableMounts(mcpMounts);
|
|
84686
87484
|
for (const mount of skippedMounts) console.warn(`[slate-bridge] skipping MCP server '${mount.serverName}' (plugin '${mount.pluginId}'): binding auth '${mount.bindingAuth.type}' is not 'authenticated_account' — excluded until the binding is flipped to a linked account; chat continues without it.`);
|
|
87485
|
+
const guestMcpPlan = await mcpGateway.resolveGuestMcpPlan(servableMounts, {
|
|
87486
|
+
userToken,
|
|
87487
|
+
workspaceId
|
|
87488
|
+
});
|
|
87489
|
+
if (ac.signal.aborted) return;
|
|
84687
87490
|
mcpSessionToken = randomUUID();
|
|
84688
87491
|
mcpGateway.registerSession(mcpSessionToken, {
|
|
84689
87492
|
userToken,
|
|
84690
87493
|
workspaceId
|
|
84691
87494
|
});
|
|
84692
|
-
if (ac.signal.aborted) return;
|
|
84693
87495
|
session = await createClaudeMicrosandbox({
|
|
84694
87496
|
claudeHomeHostDir: claudeRepo.hostDir,
|
|
84695
87497
|
hostClaudeJsonPath,
|
|
@@ -84714,9 +87516,10 @@ async function createBridgeServer(opts) {
|
|
|
84714
87516
|
},
|
|
84715
87517
|
mcpConfigArgs: buildMcpConfigArgs({
|
|
84716
87518
|
bridgeOrigin: `http://127.0.0.1:${boundPort}`,
|
|
84717
|
-
serverNames:
|
|
87519
|
+
serverNames: guestMcpPlan.serverNames,
|
|
84718
87520
|
sessionToken: mcpSessionToken
|
|
84719
87521
|
}),
|
|
87522
|
+
mcpToolRules: guestMcpPlan.toolRules,
|
|
84720
87523
|
model: plan.model ?? defaultModel,
|
|
84721
87524
|
sessionFlag
|
|
84722
87525
|
});
|
|
@@ -84745,20 +87548,43 @@ async function createBridgeServer(opts) {
|
|
|
84745
87548
|
const guestMemory = session?.guestMemory();
|
|
84746
87549
|
console.error("[slate-bridge] connection handler error:", err, { guestMemory });
|
|
84747
87550
|
Sentry.captureException(err, { extra: { guestMemory } });
|
|
84748
|
-
if (phase === "boot")
|
|
84749
|
-
else
|
|
87551
|
+
if (phase === "boot") wsCloseWithErrorFrame(ws, 4500, "session boot failed");
|
|
87552
|
+
else wsCloseWithErrorFrame(ws, 4501, "session turn failed");
|
|
84750
87553
|
} finally {
|
|
84751
87554
|
claudeRepo.releaseSession(sessionId);
|
|
84752
87555
|
ac.abort();
|
|
84753
|
-
|
|
84754
|
-
|
|
84755
|
-
|
|
84756
|
-
|
|
87556
|
+
try {
|
|
87557
|
+
if (session) await session.dispose();
|
|
87558
|
+
if (phase === "turn") claudeRepo.capture({ message: `chat ${repoId} (final)` }).catch((err) => {
|
|
87559
|
+
console.error(`[slate-bridge] final capture failed (${repoId}):`, err);
|
|
87560
|
+
});
|
|
87561
|
+
} finally {
|
|
87562
|
+
if (mcpSessionToken) mcpGateway.deregisterSession(mcpSessionToken);
|
|
87563
|
+
await claudeRepo.settle();
|
|
87564
|
+
}
|
|
84757
87565
|
console.log("[slate-bridge] connection closed");
|
|
84758
87566
|
}
|
|
84759
87567
|
})();
|
|
84760
87568
|
inflight.add(work);
|
|
84761
|
-
|
|
87569
|
+
const liveChatSession = {
|
|
87570
|
+
done: work,
|
|
87571
|
+
key,
|
|
87572
|
+
stop: () => {
|
|
87573
|
+
deletionRequested = true;
|
|
87574
|
+
wsCloseWithErrorFrame(ws, 4410, "session deleted");
|
|
87575
|
+
ac.abort();
|
|
87576
|
+
}
|
|
87577
|
+
};
|
|
87578
|
+
liveChatSessions.add(liveChatSession);
|
|
87579
|
+
const forgetLiveChatSession = () => {
|
|
87580
|
+
inflight.delete(work);
|
|
87581
|
+
liveChatSessions.delete(liveChatSession);
|
|
87582
|
+
};
|
|
87583
|
+
work.then(forgetLiveChatSession, (err) => {
|
|
87584
|
+
forgetLiveChatSession();
|
|
87585
|
+
console.error("[slate-bridge] connection teardown error:", err);
|
|
87586
|
+
Sentry.captureException(err);
|
|
87587
|
+
});
|
|
84762
87588
|
return {
|
|
84763
87589
|
onClose() {
|
|
84764
87590
|
claudeRepo.releaseSession(sessionId);
|
|
@@ -84774,6 +87600,7 @@ async function createBridgeServer(opts) {
|
|
|
84774
87600
|
},
|
|
84775
87601
|
onOpen(_evt, wsCtx) {
|
|
84776
87602
|
ws = wsCtx;
|
|
87603
|
+
if (deletionRequested) wsCloseWithErrorFrame(wsCtx, 4410, "session deleted");
|
|
84777
87604
|
}
|
|
84778
87605
|
};
|
|
84779
87606
|
}));
|
|
@@ -84791,6 +87618,7 @@ async function createBridgeServer(opts) {
|
|
|
84791
87618
|
boundPort = address.port;
|
|
84792
87619
|
return {
|
|
84793
87620
|
async close() {
|
|
87621
|
+
const closingConnections = [...inflight];
|
|
84794
87622
|
for (const client of wss.clients) client.close(1001, "server shutting down");
|
|
84795
87623
|
await new Promise((resolve, reject) => {
|
|
84796
87624
|
wss.close((err) => err ? reject(err) : resolve());
|
|
@@ -84798,7 +87626,7 @@ async function createBridgeServer(opts) {
|
|
|
84798
87626
|
await new Promise((resolve, reject) => {
|
|
84799
87627
|
server.close((err) => err ? reject(err) : resolve());
|
|
84800
87628
|
});
|
|
84801
|
-
await
|
|
87629
|
+
await joinAll(closingConnections);
|
|
84802
87630
|
},
|
|
84803
87631
|
port: boundPort
|
|
84804
87632
|
};
|
|
@@ -88924,7 +91752,7 @@ function createProgram() {
|
|
|
88924
91752
|
program.command("bridge").description("Slate bridge commands").command("start").description("Start the slate bridge server in the foreground, bound to localhost on CTXS_BRIDGE_PORT (default 7777). Authenticates each connection's aud=ctx user token and resolves its workspace + credentials from the platform (CTX_WEB_URL verifies the token, CTX_PLATFORM_URL serves the plan + base DSN); serves the render endpoints + the per-session WebSocket that spawns claude inside a microsandbox sandbox. Blocks until SIGINT/SIGTERM.").action(async () => {
|
|
88925
91753
|
await createBridgeServer({});
|
|
88926
91754
|
});
|
|
88927
|
-
program.command("parity").description("Translator parity golden tooling (dev/maintainer)").command("generate [scenarios...]").description("Drive scripted multi-turn conversations through the real bridge + microsandbox + claude and (re)write the parity goldens consumed by slate-shared's parity test. Pass scenario names to capture only those (others keep their existing goldens); omit to regenerate all. Dev tool: needs local claude auth and the microsandbox runtime. Run from source ('
|
|
91755
|
+
program.command("parity").description("Translator parity golden tooling (dev/maintainer)").command("generate [scenarios...]").description("Drive scripted multi-turn conversations through the real bridge + microsandbox + claude and (re)write the parity goldens consumed by slate-shared's parity test. Pass scenario names to capture only those (others keep their existing goldens); omit to regenerate all. Dev tool: needs local claude auth and the microsandbox runtime. Run from source ('node --import tsx cli.ts parity generate [names...]').").action(async (scenarios) => {
|
|
88928
91756
|
await generateParityGoldens({ only: scenarios });
|
|
88929
91757
|
});
|
|
88930
91758
|
program.command("sandbox").allowUnknownOption(true).allowExcessArguments(true).argument("[command...]", "Command + args to run in the guest (default: uname -a)").requiredOption("--image <ref>", "Guest image to boot in the microVM").description("Boot a microsandbox microVM from --image, run a command inside it, print its output, and tear it down. A DB-free check that the host can boot guest microVMs (/dev/kvm in a container; HVF on macOS).").action(async (command, options) => {
|
|
@@ -88950,4 +91778,4 @@ runCli().catch((error) => {
|
|
|
88950
91778
|
//#endregion
|
|
88951
91779
|
export { createProgram, runCli };
|
|
88952
91780
|
//# sourceMappingURL=cli.mjs.map
|
|
88953
|
-
//# debugId=
|
|
91781
|
+
//# debugId=3bf0713e-bd74-5249-b278-e30de2c2d243
|