@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 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]="1fde104d-ada4-505b-9cff-03f35e9d5ed1")}catch(e){}}();
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.14";
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`. A no-op if
5092
- * nothing changed (a snapshot may find no new bytes under eventual consistency).
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() === "") return;
5114
- await runGit([
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
- * Writes do NOT go through ClaudeRepo — claude writes the mounted dir directly, the
5412
- * filesystem is live truth, and ClaudeRepo periodically snapshots it (eventual
5413
- * consistency). A snapshot may capture MID-WRITE state — a pushed transcript can end in
5414
- * a torn half-line — so raw-home readers must tolerate partial tails (the index reader
5415
- * does). Correctness rests on one repoId ↔ one owning process ↔ this one singleton,
5416
- * so the only concurrency it must handle is one user's multiple sessions, which the
5417
- * mutex serializes.
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). Capture failure is caught +
5522
- * logged **loud**, never propagated — the bytes are already on disk and the next snapshot
5523
- * pushes the cumulative state. The one non-self-healing case is an expired token's 401:
5524
- * the push auth is the connection's upgrade-time user token (real mint TTL: 15 minutes),
5525
- * so every capture in a chat older than that fails until the user reconnects — and the
5526
- * turns since the last good push are LOST outright if this host dir dies first. Accepted
5527
- * for day one (user, 2026-07-09); problem + required outcome tracked in the internal
5528
- * planned note "chat session token expiry" (2026-07-09).
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
- try {
5533
- await this.ensureGitignore();
5534
- if (this.config.maintainIndex) await this.writeSessionIndex();
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 (capture already contains its own failures).
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$1 = class ParseError extends Error {
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$1(parsed.errors);
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: { type: "client_credentials" },
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 = 4; at < seconds; at += 4) instants.push(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 write and hand back the receipt.
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 nothing and is
56265
- * still open, so it would sit against the caller's open-set quota forever with no
56266
- * API to find it again; abandoning an empty set is accepted. After execute the
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 submitWrite(api, input) {
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
- await api.stage({
56282
- operationSetId,
56283
- write: input.write
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
- * A NEW project task. Every column is written, so nothing here is optional — and
56368
- * the primary key is ours to choose: the service honours a client-supplied
56369
- * `msdyn_projecttaskid`, which is what lets a caller know the id before the write
56370
- * has even been executed.
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
- "@odata.type": projectTaskType,
56376
- msdyn_outlinelevel: 1,
56377
- "msdyn_project@odata.bind": `/msdyn_projects(${fields.projectId})`,
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
- * A CHANGE to an existing task: only the named fields are written, which is what
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
- * That guarantee is worth keeping for a reason the wire made plain: asked to move a
56391
- * task into a bucket belonging to ANOTHER project, the service does not refuse. It
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 updateProjectTask(fields) {
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
- "@odata.type": projectTaskType,
56400
- msdyn_projecttaskid: fields.taskId,
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 Error(`Schedule API ${path} failed: ${response.status} ${response.statusText}${detail ? ` — ${detail}` : ""}`);
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.write.verb === "create" ? "msdyn_PssCreateV1" : "msdyn_PssUpdateV1", {
56459
- Entity: input.write.entity,
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.14";
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 write tools take it: the GUID does the work, the name is
56467
- * what the human reads on the approval card. They are ONE object because they are
56468
- * one concept — which makes "an id without its label" unrepresentable in the
56469
- * advertised schema, rather than a runtime check that fires after a human has
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 `create_task` at argument validation. `guid()`
56475
- * still enforces the 8-4-4-4-12 shape. Do not tighten.
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 reference = (what, idColumn) => strictObject({
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
- const RECEIPT = "Returns an operation_set_id — a RECEIPT, not a confirmation: the write has been accepted, not applied. Pass it to wait_for_operation_set to learn whether it landed.";
56503
- const SAME_PROJECT = "The bucket must belong to the named project. A bucket from another project is NOT rejected — the service reports the set completed anyway, and where the write landed, when checked, was a bucket nobody asked for.";
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("create_task", {
56510
- description: `Create a task on a Premium Planner (Project for the web) board, in a specific bucket. Read the ids from the synced msdyn_projects / msdyn_projectbuckets tables; each id travels with the matching name, which is what the human approving this action reads. ${SAME_PROJECT} ${RECEIPT} It also returns task_id — the id the task WILL have if the set completes.`,
56511
- inputSchema: strictObject({
56512
- bucket: reference("bucket", "msdyn_projectbucketid"),
56513
- name: string$3().min(1).describe("The new task's name"),
56514
- project: reference("project", "msdyn_projectid")
56515
- })
56516
- }, async ({ bucket, name, project }, extra) => {
56517
- const taskId = randomUUID();
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
- operation_set_id: await submitWrite(scheduleApiFromRequest(extra.requestInfo), {
56520
- description: `ContextLayer create_task: ${name}`,
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 write. Takes the operation_set_id returned by create_task or update_task and reads it until it settles or the window closes, then reports the scheduling service's own status: completed, failed (with its reason), executing, open, or abandoned. Writes usually settle within a few seconds; the slowest recorded took about half a minute. Note that 'completed' means the service applied the set without error — it is not a promise that a field holds the value you sent.",
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 create_task or update_task"),
56562
- seconds: number$4().int().min(0).max(45).describe(`How long to wait, 0-45. 0 reads once and returns whatever it says. Still 'executing' when the window closes means the write is in flight, NOT lost — call again.`)
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-BiX3rFo-.mjs
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.14"),
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 Object.entries(input.moduleServers)) {
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 disposed = false;
86934
+ let disposePromise = null;
84200
86935
  return {
84201
- dispose: async () => {
84202
- if (disposed) return;
84203
- disposed = true;
84204
- guestMemory.stop();
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 wsFailClose(ws, code, reason) {
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
- const repoId = buildRepoId({
84639
- kind: "claude",
84640
- owner: "slate",
84641
- userId: r.sub,
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
- const claudeRepo = ClaudeRepo.get(await resolveRepo({
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
- repoId,
84647
- token: userToken
84648
- }, gitHostBaseDir), {
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: servableMounts.map((mount) => mount.serverName),
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") wsFailClose(ws, 4500, "session boot failed");
84749
- else wsFailClose(ws, 4501, "session turn failed");
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
- if (session) await session.dispose();
84754
- if (mcpSessionToken) mcpGateway.deregisterSession(mcpSessionToken);
84755
- if (phase === "turn") claudeRepo.capture({ message: `chat ${repoId} (final)` });
84756
- await claudeRepo.settle();
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
- work.finally(() => inflight.delete(work));
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 Promise.all([...inflight]);
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 ('npx tsx cli.ts parity generate [names...]').").action(async (scenarios) => {
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=1fde104d-ada4-505b-9cff-03f35e9d5ed1
91781
+ //# debugId=3bf0713e-bd74-5249-b278-e30de2c2d243