@sema-agent/core 5.5.0 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.6.0 (2026-08-03)
4
+
5
+ _A hardening tail: numeric knobs refuse what they cannot honor, the binary gate reads bytes instead of trusting statistics, and the MCP SDK takes its last zero-cost step before the v2 jump._
6
+
7
+ ### Fixed
8
+
9
+ - Stall-timeout knobs (`connectTimeoutMs` / `firstTokenTimeoutMs` / `idleTimeoutMs`) refuse invalid numbers loudly (`config.stall_timeout_invalid`) instead of silently disarming — a NaN (units fumble: `"120s"` → `Number` → NaN) used to read as "unset", switching every watchdog OFF exactly when a deployment meant to tighten them. `0` stays legal as an explicit off; absent stays absent.
10
+
11
+ - Read's binary-extension list gains magic-byte criteria of its own (~40 formats, one data-driven signature table): a file whose bytes carry a real binary header (ZIP/gzip/7z/ELF/tar/RIFF…) is now refused by CONTENT even when printable padding used to slip it past the statistical sniffer as mojibake "text". Names still never decide: a `.zip` holding plain text still reads. The Edit/Write/NotebookEdit read-refusal texts all name the same escape route (one constant; NotebookEdit used to render the bare sentence with no hint at all).
12
+
13
+ ### Changed
14
+
15
+ - `@modelcontextprotocol/sdk` 1.29.0 → 1.30.0 (exact pin). Public surface is purely additive upstream (stdio `maxBufferSize`, SSE `keepAliveMs`); `types` is byte-identical; the only behavior delta is a richer zod parse-error message the engine never pins on. Groundwork for the v2 three-package migration (spiked, GO, waiting out the first patch release).
16
+
3
17
  ## 5.5.0 (2026-08-03)
4
18
 
5
19
  _Three follow-through items: the deferred-placeholder wrong-guess round trip halves, durable cron jobs survive their creating session, and the redaction findings channel closes its last three audit gaps._
@@ -2,7 +2,7 @@ import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
2
  import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
3
3
  import { retryBackoffMs } from "./retry.js";
4
4
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
5
- import { createConnectController } from "./timeout.js";
5
+ import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
6
6
  import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
7
7
  const DEFAULT_MAX_RETRIES = 10;
8
8
  const MAX_RETRIES_ENV_CEILING = 15;
@@ -113,9 +113,9 @@ export function runStreamingBrain(args) {
113
113
  const req = buildRequest();
114
114
  const maxRetries = resolveMaxRetries(config.maxRetries);
115
115
  const baseDelay = config.retryDelayMs ?? 500;
116
- const firstTokenTimeoutMs = config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs;
117
- const idleTimeoutMs = config.idleTimeoutMs ?? stallTimeouts?.idleMs;
118
- const connectTimeoutMs = config.connectTimeoutMs ?? stallTimeouts?.connectMs;
116
+ const firstTokenTimeoutMs = resolveStallTimeoutMs(config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs, "firstTokenTimeoutMs");
117
+ const idleTimeoutMs = resolveStallTimeoutMs(config.idleTimeoutMs ?? stallTimeouts?.idleMs, "idleTimeoutMs");
118
+ const connectTimeoutMs = resolveStallTimeoutMs(config.connectTimeoutMs ?? stallTimeouts?.connectMs, "connectTimeoutMs");
119
119
  let attempt = 0;
120
120
  let thinkingRetries = 0;
121
121
  let startEmitted = false;
@@ -1,4 +1,5 @@
1
1
  import type { StreamFn } from "../internal/llm.js";
2
+ export declare function resolveStallTimeoutMs(value: number | undefined, knob: string): number | undefined;
2
3
  export interface BrainTimeoutConfig {
3
4
  connectTimeoutMs?: number;
4
5
  firstTokenTimeoutMs?: number;
@@ -1,3 +1,13 @@
1
+ export function resolveStallTimeoutMs(value, knob) {
2
+ if (value === undefined)
3
+ return undefined;
4
+ if (!Number.isFinite(value) || value < 0) {
5
+ const e = new Error(`${knob} must be a non-negative finite number of milliseconds (got ${String(value)})`);
6
+ e.code = "config.stall_timeout_invalid";
7
+ throw e;
8
+ }
9
+ return value;
10
+ }
1
11
  export function createConnectController(connectTimeoutMs, outerSignal, deadlineAtMs) {
2
12
  const ac = new AbortController();
3
13
  let timedOut = false;
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool, errorResult } from "../../core/tools.js";
3
- import { sha256, resolveKey, violationText, violationDetails, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
3
+ import { sha256, resolveKey, violationText, violationDetails, binaryMagicFormat, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, } from "./safety.js";
4
4
  import { decodeTextBytes } from "./encoding.js";
5
5
  import { isNotebookPath, parseNotebookCells, renderNotebookCells, stripNotebookImageData, NOTEBOOK_IMAGE_BASE64_BUDGET } from "./notebook.js";
6
6
  import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE } from "../../core/mcp.js";
@@ -166,6 +166,11 @@ export function createReadFileTool(env, state, rootCanonical, cwdRef, additional
166
166
  if (pdfMagicMatches(readBin.value)) {
167
167
  return pdfResultToToolReturn(await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities));
168
168
  }
169
+ const magicFormat = binaryMagicFormat(readBin.value);
170
+ if (magicFormat !== undefined) {
171
+ return errorResult(`Error (Read): "${path}" appears to be a binary file (${magicFormat}, identified by its file signature — whatever its extension says); ` +
172
+ `this tool reads UTF-8 and BOM-marked UTF-16LE text only. Inspect or convert it with bash instead.`);
173
+ }
169
174
  const decoded = decodeTextBytes(readBin.value);
170
175
  if (decoded.malformed) {
171
176
  return errorResult(`Error (Read): "${path}" has a UTF-16 BOM but a truncated (odd-length) body — the file is corrupt or mis-labelled; repair/convert it with bash (e.g. \`iconv\`) first.`);
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "../../core/tools.js";
4
- import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT, } from "./safety.js";
4
+ import { sha256, resolveKey, violationText, violationDetails, requireRead, checkStale, checkEditMatch, checkNoChange, fileArgPath, resolveQuoteMatch, adaptNewStringQuotes, resolveEscapeMatch, adaptNewStringEscapes, escapeMatchWasAttempted, ESCAPE_MATCH_MISS_NOTE, deletionOldString, countOccurrences, READ_REFUSED_ESCAPE_HINT, } from "./safety.js";
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, formatByteSize, decodeEditBytes, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, } from "./fs-shared.js";
7
7
  async function gateToolWrite(hook, tool, path, key, content) {
@@ -136,7 +136,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
136
136
  return errorResult(ipynb);
137
137
  const notRead = requireRead(state, r.key);
138
138
  if (notRead)
139
- return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal));
139
+ return errorResult(await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
140
140
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
141
141
  if (!readBin.ok)
142
142
  return errorResult(`Error (Edit): cannot read "${path}": ${readBin.error.message}`);
@@ -249,7 +249,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
249
249
  if (exists.value) {
250
250
  const notRead = requireRead(state, r.key);
251
251
  if (notRead) {
252
- return errorResult(await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal, WRITE_ENCODING_DEADLOCK_ESCAPE_HINT));
252
+ return errorResult(await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
253
253
  }
254
254
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
255
255
  if (!readBin.ok)
@@ -334,7 +334,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
334
334
  return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
335
335
  const notRead = requireRead(state, r.key);
336
336
  if (notRead)
337
- return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal));
337
+ return errorResult(await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT));
338
338
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
339
339
  if (!readBin.ok)
340
340
  return errorResult(`Error (NotebookEdit): cannot read "${notebook_path}": ${readBin.error.message}`);
@@ -30,6 +30,16 @@ export declare function foldRedundantPathSegments(p: string): string;
30
30
  export declare function hasBinaryExtension(path: string): boolean;
31
31
  export declare function imageMimeForRead(path: string): string | undefined;
32
32
  export declare function imageMagicMatches(bytes: Uint8Array, mimeType: string): boolean;
33
+ export interface BinaryMagicSegment {
34
+ readonly offset: number;
35
+ readonly bytes: readonly number[];
36
+ }
37
+ export interface BinaryMagicSignature {
38
+ readonly format: string;
39
+ readonly segments: readonly BinaryMagicSegment[];
40
+ }
41
+ export declare const BINARY_MAGIC_SIGNATURES: readonly BinaryMagicSignature[];
42
+ export declare function binaryMagicFormat(bytes: Uint8Array): string | undefined;
33
43
  export declare function isBinaryContent(sample: string): boolean;
34
44
  export declare function isUncPath(path: string): boolean;
35
45
  export declare function resolveKey(env: ExecutionEnv, rootCanonical: string, path: string, signal?: AbortSignal, baseCwd?: string, additionalRootsCanonical?: readonly string[], exactFileReadExemption?: (canonicalKey: string) => boolean): Promise<{
@@ -59,7 +69,7 @@ export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to t
59
69
  export declare function requireRead(state: ReadFileState, key: string): FsViolation | undefined;
60
70
  export declare const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused \u2014 read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
61
71
  export declare const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view \u2014 the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
62
- export declare const WRITE_ENCODING_DEADLOCK_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead \u2014 e.g. `rm` + rewrite, or `iconv`.)";
72
+ export declare const READ_REFUSED_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead \u2014 e.g. `rm` + rewrite, or `iconv`.)";
63
73
  export declare function checkNoChange(oldString: string, newString: string): FsViolation | undefined;
64
74
  export declare function checkStale(entry: ReadEntry | undefined, currentHash: string): FsViolation | undefined;
65
75
  export declare function countOccurrences(haystack: string, needle: string): number;
@@ -150,6 +150,89 @@ export function imageMagicMatches(bytes, mimeType) {
150
150
  return false;
151
151
  }
152
152
  }
153
+ const asciiBytes = (s) => Array.from(s, (c) => c.charCodeAt(0));
154
+ export const BINARY_MAGIC_SIGNATURES = [
155
+ { format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x03, 0x04] }] },
156
+ { format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x05, 0x06] }] },
157
+ { format: "ZIP archive", segments: [{ offset: 0, bytes: [0x50, 0x4b, 0x07, 0x08] }] },
158
+ { format: "gzip archive", segments: [{ offset: 0, bytes: [0x1f, 0x8b, 0x08] }] },
159
+ ...[0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39].map((level) => ({
160
+ format: "bzip2 archive",
161
+ segments: [{ offset: 0, bytes: [0x42, 0x5a, 0x68, level] }],
162
+ })),
163
+ { format: "xz archive", segments: [{ offset: 0, bytes: [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00] }] },
164
+ { format: "zstd archive", segments: [{ offset: 0, bytes: [0x28, 0xb5, 0x2f, 0xfd] }] },
165
+ { format: "lz4 archive", segments: [{ offset: 0, bytes: [0x04, 0x22, 0x4d, 0x18] }] },
166
+ { format: "compress archive", segments: [{ offset: 0, bytes: [0x1f, 0x9d] }] },
167
+ { format: "7-Zip archive", segments: [{ offset: 0, bytes: [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c] }] },
168
+ { format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x00] }] },
169
+ { format: "RAR archive", segments: [{ offset: 0, bytes: [0x52, 0x61, 0x72, 0x21, 0x1a, 0x07, 0x01, 0x00] }] },
170
+ { format: "tar archive", segments: [{ offset: 257, bytes: asciiBytes("ustar") }] },
171
+ { format: "ar archive (static library or Debian package)", segments: [{ offset: 0, bytes: asciiBytes("!<arch>\n") }] },
172
+ { format: "RPM package", segments: [{ offset: 0, bytes: [0xed, 0xab, 0xee, 0xdb] }] },
173
+ { format: "ISO 9660 disc image", segments: [{ offset: 32769, bytes: asciiBytes("CD001") }] },
174
+ { format: "PNG image", segments: [{ offset: 0, bytes: [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] }] },
175
+ { format: "JPEG image", segments: [{ offset: 0, bytes: [0xff, 0xd8, 0xff] }] },
176
+ { format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF87a") }] },
177
+ { format: "GIF image", segments: [{ offset: 0, bytes: asciiBytes("GIF89a") }] },
178
+ { format: "TIFF image", segments: [{ offset: 0, bytes: [0x49, 0x49, 0x2a, 0x00] }] },
179
+ { format: "TIFF image", segments: [{ offset: 0, bytes: [0x4d, 0x4d, 0x00, 0x2a] }] },
180
+ { format: "BMP image", segments: [{ offset: 0, bytes: asciiBytes("BM") }, { offset: 6, bytes: [0x00, 0x00, 0x00, 0x00] }] },
181
+ { format: "ICO image", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0x00] }] },
182
+ { format: "Photoshop document", segments: [{ offset: 0, bytes: asciiBytes("8BPS") }] },
183
+ { format: "WebP image", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WEBP") }] },
184
+ { format: "RIFF audio (WAV)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("WAVE") }] },
185
+ { format: "RIFF video (AVI)", segments: [{ offset: 0, bytes: asciiBytes("RIFF") }, { offset: 8, bytes: asciiBytes("AVI ") }] },
186
+ { format: "AIFF audio", segments: [{ offset: 0, bytes: asciiBytes("FORM") }, { offset: 8, bytes: asciiBytes("AIFF") }] },
187
+ { format: "Ogg container", segments: [{ offset: 0, bytes: asciiBytes("OggS") }, { offset: 4, bytes: [0x00] }] },
188
+ { format: "FLAC audio", segments: [{ offset: 0, bytes: asciiBytes("fLaC") }] },
189
+ ...[0x02, 0x03, 0x04].map((major) => ({
190
+ format: "MP3 audio (ID3 tag)",
191
+ segments: [{ offset: 0, bytes: [0x49, 0x44, 0x33, major] }],
192
+ })),
193
+ { format: "ISO base media container (MP4/MOV/HEIC)", segments: [{ offset: 4, bytes: asciiBytes("ftyp") }] },
194
+ { format: "Matroska/WebM container", segments: [{ offset: 0, bytes: [0x1a, 0x45, 0xdf, 0xa3] }] },
195
+ { format: "ASF container (WMV/WMA)", segments: [{ offset: 0, bytes: [0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c] }] },
196
+ { format: "FLV video", segments: [{ offset: 0, bytes: [0x46, 0x4c, 0x56, 0x01] }] },
197
+ { format: "MPEG program stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xba] }] },
198
+ { format: "MPEG video stream", segments: [{ offset: 0, bytes: [0x00, 0x00, 0x01, 0xb3] }] },
199
+ { format: "ELF binary", segments: [{ offset: 0, bytes: [0x7f, 0x45, 0x4c, 0x46] }] },
200
+ { format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xce] }] },
201
+ { format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xfe, 0xed, 0xfa, 0xcf] }] },
202
+ { format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xce, 0xfa, 0xed, 0xfe] }] },
203
+ { format: "Mach-O binary", segments: [{ offset: 0, bytes: [0xcf, 0xfa, 0xed, 0xfe] }] },
204
+ { format: "Java class file or Mach-O universal binary", segments: [{ offset: 0, bytes: [0xca, 0xfe, 0xba, 0xbe] }] },
205
+ { format: "WebAssembly module", segments: [{ offset: 0, bytes: [0x00, 0x61, 0x73, 0x6d] }] },
206
+ { format: "OLE compound document (Office 97-2003 or MSI)", segments: [{ offset: 0, bytes: [0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1] }] },
207
+ { format: "SQLite database", segments: [{ offset: 0, bytes: [...asciiBytes("SQLite format 3"), 0x00] }] },
208
+ { format: "WOFF font", segments: [{ offset: 0, bytes: asciiBytes("wOFF") }] },
209
+ { format: "WOFF2 font", segments: [{ offset: 0, bytes: asciiBytes("wOF2") }] },
210
+ { format: "OpenType font", segments: [{ offset: 0, bytes: asciiBytes("OTTO") }] },
211
+ { format: "TrueType font", segments: [{ offset: 0, bytes: [0x00, 0x01, 0x00, 0x00] }] },
212
+ { format: "TrueType font collection", segments: [{ offset: 0, bytes: asciiBytes("ttcf") }] },
213
+ ];
214
+ export function binaryMagicFormat(bytes) {
215
+ for (const signature of BINARY_MAGIC_SIGNATURES) {
216
+ let matched = true;
217
+ for (const segment of signature.segments) {
218
+ if (bytes.length < segment.offset + segment.bytes.length) {
219
+ matched = false;
220
+ break;
221
+ }
222
+ for (let i = 0; i < segment.bytes.length; i++) {
223
+ if (bytes[segment.offset + i] !== segment.bytes[i]) {
224
+ matched = false;
225
+ break;
226
+ }
227
+ }
228
+ if (!matched)
229
+ break;
230
+ }
231
+ if (matched)
232
+ return signature.format;
233
+ }
234
+ return undefined;
235
+ }
153
236
  export function isBinaryContent(sample) {
154
237
  if (sample.length === 0)
155
238
  return false;
@@ -297,7 +380,7 @@ export function requireRead(state, key) {
297
380
  }
298
381
  export const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused — read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
299
382
  export const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view — the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
300
- export const WRITE_ENCODING_DEADLOCK_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. `rm` + rewrite, or `iconv`.)";
383
+ export const READ_REFUSED_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. `rm` + rewrite, or `iconv`.)";
301
384
  export function checkNoChange(oldString, newString) {
302
385
  if (oldString === newString) {
303
386
  return { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "5.5.0",
3
+ "version": "5.6.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -67,7 +67,7 @@
67
67
  "gate:single-mint": "node scripts/verify-single-mint.mjs"
68
68
  },
69
69
  "dependencies": {
70
- "@modelcontextprotocol/sdk": "1.29.0",
70
+ "@modelcontextprotocol/sdk": "1.30.0",
71
71
  "typebox": "1.1.39"
72
72
  },
73
73
  "optionalDependencies": {