@namzu/sdk 39.0.0 → 41.0.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 +328 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +8 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +11 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/connector/index.d.ts +2 -2
- package/dist/connector/index.d.ts.map +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/connector/index.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +112 -10
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/audio-admission.d.ts +17 -0
- package/dist/connector/mcp/audio-admission.d.ts.map +1 -0
- package/dist/connector/mcp/audio-admission.js +171 -0
- package/dist/connector/mcp/audio-admission.js.map +1 -0
- package/dist/connector/mcp/client.d.ts +252 -1
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +611 -39
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/connector/mcp/envelope.d.ts +91 -0
- package/dist/connector/mcp/envelope.d.ts.map +1 -0
- package/dist/connector/mcp/envelope.js +173 -0
- package/dist/connector/mcp/envelope.js.map +1 -0
- package/dist/connector/mcp/era.d.ts +130 -0
- package/dist/connector/mcp/era.d.ts.map +1 -0
- package/dist/connector/mcp/era.js +304 -0
- package/dist/connector/mcp/era.js.map +1 -0
- package/dist/connector/mcp/errors.d.ts +106 -0
- package/dist/connector/mcp/errors.d.ts.map +1 -0
- package/dist/connector/mcp/errors.js +154 -0
- package/dist/connector/mcp/errors.js.map +1 -0
- package/dist/connector/mcp/http-sse.d.ts +11 -0
- package/dist/connector/mcp/http-sse.d.ts.map +1 -1
- package/dist/connector/mcp/http-sse.js +21 -6
- package/dist/connector/mcp/http-sse.js.map +1 -1
- package/dist/connector/mcp/index.d.ts +7 -0
- package/dist/connector/mcp/index.d.ts.map +1 -1
- package/dist/connector/mcp/index.js +10 -0
- package/dist/connector/mcp/index.js.map +1 -1
- package/dist/connector/mcp/streamable-http.d.ts +83 -0
- package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
- package/dist/connector/mcp/streamable-http.js +177 -11
- package/dist/connector/mcp/streamable-http.js.map +1 -1
- package/dist/connector/mcp/x-mcp-header.d.ts +56 -0
- package/dist/connector/mcp/x-mcp-header.d.ts.map +1 -0
- package/dist/connector/mcp/x-mcp-header.js +254 -0
- package/dist/connector/mcp/x-mcp-header.js.map +1 -0
- package/dist/constants/mcp/index.d.ts +123 -15
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +135 -16
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/manager/agent/lifecycle.d.ts.map +1 -1
- package/dist/manager/agent/lifecycle.js +23 -0
- package/dist/manager/agent/lifecycle.js.map +1 -1
- package/dist/manager/run/persistence.d.ts +8 -0
- package/dist/manager/run/persistence.d.ts.map +1 -1
- package/dist/manager/run/persistence.js +12 -0
- package/dist/manager/run/persistence.js.map +1 -1
- package/dist/prompt/coding-agent-doctrine.d.ts +20 -0
- package/dist/prompt/coding-agent-doctrine.d.ts.map +1 -1
- package/dist/prompt/coding-agent-doctrine.js +19 -3
- package/dist/prompt/coding-agent-doctrine.js.map +1 -1
- package/dist/prompt/index.d.ts +1 -1
- package/dist/prompt/index.d.ts.map +1 -1
- package/dist/prompt/index.js +1 -1
- package/dist/prompt/index.js.map +1 -1
- package/dist/public-runtime.d.ts +6 -4
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +8 -4
- package/dist/public-runtime.js.map +1 -1
- package/dist/public-tools.d.ts +11 -0
- package/dist/public-tools.d.ts.map +1 -1
- package/dist/public-tools.js +14 -0
- package/dist/public-tools.js.map +1 -1
- package/dist/registry/tool/execute.d.ts.map +1 -1
- package/dist/registry/tool/execute.js +2 -3
- package/dist/registry/tool/execute.js.map +1 -1
- package/dist/registry/tool/portable.d.ts +65 -0
- package/dist/registry/tool/portable.d.ts.map +1 -0
- package/dist/registry/tool/portable.js +244 -0
- package/dist/registry/tool/portable.js.map +1 -0
- package/dist/registry/tool/schema.d.ts +32 -5
- package/dist/registry/tool/schema.d.ts.map +1 -1
- package/dist/registry/tool/schema.js +35 -9
- package/dist/registry/tool/schema.js.map +1 -1
- package/dist/registry/toolset/catalog.js +8 -8
- package/dist/registry/toolset/catalog.js.map +1 -1
- package/dist/runtime/jobs/awaited-jobs.d.ts +215 -0
- package/dist/runtime/jobs/awaited-jobs.d.ts.map +1 -0
- package/dist/runtime/jobs/awaited-jobs.js +259 -0
- package/dist/runtime/jobs/awaited-jobs.js.map +1 -0
- package/dist/runtime/jobs/registry.d.ts +33 -2
- package/dist/runtime/jobs/registry.d.ts.map +1 -1
- package/dist/runtime/jobs/registry.js +37 -0
- package/dist/runtime/jobs/registry.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +28 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +39 -1
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/file-evidence-context.d.ts.map +1 -1
- package/dist/runtime/query/file-evidence-context.js +159 -43
- package/dist/runtime/query/file-evidence-context.js.map +1 -1
- package/dist/runtime/query/file-evidence-replay.d.ts +260 -0
- package/dist/runtime/query/file-evidence-replay.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-replay.js +647 -0
- package/dist/runtime/query/file-evidence-replay.js.map +1 -0
- package/dist/runtime/query/file-evidence-seed.d.ts +50 -0
- package/dist/runtime/query/file-evidence-seed.d.ts.map +1 -0
- package/dist/runtime/query/file-evidence-seed.js +100 -0
- package/dist/runtime/query/file-evidence-seed.js.map +1 -0
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +94 -2
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/index.d.ts +87 -9
- package/dist/runtime/query/iteration/index.d.ts.map +1 -1
- package/dist/runtime/query/iteration/index.js +193 -28
- package/dist/runtime/query/iteration/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
- package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/context.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +5 -1
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/plugin-hooks.d.ts +14 -0
- package/dist/runtime/query/plugin-hooks.d.ts.map +1 -1
- package/dist/runtime/query/plugin-hooks.js +18 -0
- package/dist/runtime/query/plugin-hooks.js.map +1 -1
- package/dist/runtime/query/repeat-call.d.ts +17 -4
- package/dist/runtime/query/repeat-call.d.ts.map +1 -1
- package/dist/runtime/query/repeat-call.js +26 -19
- package/dist/runtime/query/repeat-call.js.map +1 -1
- package/dist/runtime/query/steering.d.ts +11 -1
- package/dist/runtime/query/steering.d.ts.map +1 -1
- package/dist/runtime/query/steering.js +12 -1
- package/dist/runtime/query/steering.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +46 -3
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/scheduler/completion-inbox.d.ts +48 -2
- package/dist/scheduler/completion-inbox.d.ts.map +1 -1
- package/dist/scheduler/completion-inbox.js +102 -10
- package/dist/scheduler/completion-inbox.js.map +1 -1
- package/dist/scheduler/local.d.ts.map +1 -1
- package/dist/scheduler/local.js +8 -0
- package/dist/scheduler/local.js.map +1 -1
- package/dist/store/run/disk.d.ts +35 -1
- package/dist/store/run/disk.d.ts.map +1 -1
- package/dist/store/run/disk.js +100 -0
- package/dist/store/run/disk.js.map +1 -1
- package/dist/tools/builtins/bash.d.ts.map +1 -1
- package/dist/tools/builtins/bash.js +4 -10
- package/dist/tools/builtins/bash.js.map +1 -1
- package/dist/tools/builtins/edit-apply.d.ts +126 -0
- package/dist/tools/builtins/edit-apply.d.ts.map +1 -0
- package/dist/tools/builtins/edit-apply.js +360 -0
- package/dist/tools/builtins/edit-apply.js.map +1 -0
- package/dist/tools/builtins/edit.d.ts +143 -1
- package/dist/tools/builtins/edit.d.ts.map +1 -1
- package/dist/tools/builtins/edit.js +37 -219
- package/dist/tools/builtins/edit.js.map +1 -1
- package/dist/tools/builtins/index.d.ts +1 -0
- package/dist/tools/builtins/index.d.ts.map +1 -1
- package/dist/tools/builtins/index.js +9 -3
- package/dist/tools/builtins/index.js.map +1 -1
- package/dist/tools/builtins/job.js +1 -1
- package/dist/tools/builtins/job.js.map +1 -1
- package/dist/tools/builtins/read-file.d.ts +2 -2
- package/dist/tools/builtins/read-file.d.ts.map +1 -1
- package/dist/tools/builtins/read-file.js +50 -65
- package/dist/tools/builtins/read-file.js.map +1 -1
- package/dist/tools/builtins/read-render.d.ts +56 -0
- package/dist/tools/builtins/read-render.d.ts.map +1 -0
- package/dist/tools/builtins/read-render.js +73 -0
- package/dist/tools/builtins/read-render.js.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts +67 -0
- package/dist/tools/builtins/wait-for-job-bounds.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job-bounds.js +108 -0
- package/dist/tools/builtins/wait-for-job-bounds.js.map +1 -0
- package/dist/tools/builtins/wait-for-job.d.ts +6 -0
- package/dist/tools/builtins/wait-for-job.d.ts.map +1 -0
- package/dist/tools/builtins/wait-for-job.js +162 -0
- package/dist/tools/builtins/wait-for-job.js.map +1 -0
- package/dist/tools/builtins/write-file.js +5 -0
- package/dist/tools/builtins/write-file.js.map +1 -1
- package/dist/tools/coordinator/index.d.ts.map +1 -1
- package/dist/tools/coordinator/index.js +1 -7
- package/dist/tools/coordinator/index.js.map +1 -1
- package/dist/tools/file-read-tracker.d.ts.map +1 -1
- package/dist/tools/file-read-tracker.js +88 -10
- package/dist/tools/file-read-tracker.js.map +1 -1
- package/dist/types/agent/scheduler.d.ts +20 -0
- package/dist/types/agent/scheduler.d.ts.map +1 -1
- package/dist/types/agent/task.d.ts +20 -0
- package/dist/types/agent/task.d.ts.map +1 -1
- package/dist/types/connector/mcp.d.ts +205 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/message/index.d.ts +1 -1
- package/dist/types/message/index.d.ts.map +1 -1
- package/dist/types/message/index.js +2 -0
- package/dist/types/message/index.js.map +1 -1
- package/dist/types/run/entity.d.ts +13 -0
- package/dist/types/run/entity.d.ts.map +1 -1
- package/dist/types/run/events.d.ts +56 -0
- package/dist/types/run/events.d.ts.map +1 -1
- package/dist/types/run/events.js.map +1 -1
- package/dist/types/run/store.d.ts +41 -0
- package/dist/types/run/store.d.ts.map +1 -1
- package/dist/types/sandbox/index.d.ts +83 -15
- package/dist/types/sandbox/index.d.ts.map +1 -1
- package/dist/types/sandbox/index.js.map +1 -1
- package/dist/types/tool/index.d.ts +109 -0
- package/dist/types/tool/index.d.ts.map +1 -1
- package/dist/types/tool/index.js.map +1 -1
- package/dist/utils/env.d.ts +19 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +25 -0
- package/dist/utils/env.js.map +1 -0
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +8 -0
- package/src/bridge/sse/mapper.ts +11 -0
- package/src/connector/index.ts +27 -0
- package/src/connector/mcp/adapter.ts +123 -10
- package/src/connector/mcp/audio-admission.ts +173 -0
- package/src/connector/mcp/client.ts +694 -45
- package/src/connector/mcp/envelope.ts +235 -0
- package/src/connector/mcp/era.ts +400 -0
- package/src/connector/mcp/errors.ts +171 -0
- package/src/connector/mcp/http-sse.ts +23 -6
- package/src/connector/mcp/index.ts +37 -0
- package/src/connector/mcp/streamable-http.ts +199 -11
- package/src/connector/mcp/x-mcp-header.ts +322 -0
- package/src/constants/mcp/index.ts +145 -16
- package/src/manager/agent/lifecycle.ts +29 -0
- package/src/manager/run/persistence.ts +12 -0
- package/src/prompt/coding-agent-doctrine.ts +31 -4
- package/src/prompt/index.ts +1 -0
- package/src/public-runtime.ts +37 -1
- package/src/public-tools.ts +18 -0
- package/src/registry/tool/execute.ts +2 -4
- package/src/registry/tool/portable.ts +264 -0
- package/src/registry/tool/schema.ts +38 -8
- package/src/registry/toolset/catalog.ts +8 -9
- package/src/runtime/jobs/awaited-jobs.ts +271 -0
- package/src/runtime/jobs/registry.ts +50 -0
- package/src/runtime/query/executor.ts +49 -1
- package/src/runtime/query/file-evidence-context.ts +190 -46
- package/src/runtime/query/file-evidence-replay.ts +776 -0
- package/src/runtime/query/file-evidence-seed.ts +126 -0
- package/src/runtime/query/index.ts +104 -2
- package/src/runtime/query/iteration/index.ts +202 -28
- package/src/runtime/query/iteration/phases/context.ts +10 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +4 -0
- package/src/runtime/query/plugin-hooks.ts +20 -0
- package/src/runtime/query/repeat-call.ts +28 -18
- package/src/runtime/query/steering.ts +11 -0
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +45 -2
- package/src/scheduler/completion-inbox.ts +105 -9
- package/src/scheduler/local.ts +8 -0
- package/src/store/run/disk.ts +108 -0
- package/src/tools/builtins/bash.ts +4 -10
- package/src/tools/builtins/edit-apply.ts +456 -0
- package/src/tools/builtins/edit.ts +39 -270
- package/src/tools/builtins/index.ts +9 -3
- package/src/tools/builtins/job.ts +1 -1
- package/src/tools/builtins/read-file.ts +56 -77
- package/src/tools/builtins/read-render.ts +104 -0
- package/src/tools/builtins/wait-for-job-bounds.ts +179 -0
- package/src/tools/builtins/wait-for-job.ts +184 -0
- package/src/tools/builtins/write-file.ts +5 -0
- package/src/tools/coordinator/index.ts +1 -7
- package/src/tools/file-read-tracker.ts +85 -7
- package/src/types/agent/scheduler.ts +21 -0
- package/src/types/agent/task.ts +21 -0
- package/src/types/connector/mcp.ts +205 -1
- package/src/types/message/index.ts +2 -0
- package/src/types/run/entity.ts +14 -0
- package/src/types/run/events.ts +56 -0
- package/src/types/run/store.ts +42 -0
- package/src/types/sandbox/index.ts +84 -15
- package/src/types/tool/index.ts +104 -0
- package/src/utils/env.ts +23 -0
|
@@ -9,7 +9,9 @@ import type {
|
|
|
9
9
|
} from '../../types/connector/index.js'
|
|
10
10
|
import type { ToolResultBlock, ToolResultContent } from '../../types/message/index.js'
|
|
11
11
|
import type { ToolContext, ToolDefinition, ToolResult } from '../../types/tool/index.js'
|
|
12
|
+
import { admitMcpAudioBatch } from './audio-admission.js'
|
|
12
13
|
import type { MCPClient } from './client.js'
|
|
14
|
+
import { MCPInputRequiredError, isMissingRequiredClientCapabilityError } from './errors.js'
|
|
13
15
|
import { MCPHttpRedirectError } from './http-redirect.js'
|
|
14
16
|
import { admitMcpImageBatch } from './image-admission.js'
|
|
15
17
|
|
|
@@ -338,11 +340,20 @@ const MAX_TUPLE_ARITY = 32
|
|
|
338
340
|
* down every run that offered the toolset. A faithful conversion that cannot
|
|
339
341
|
* be sent is strictly worse than a lossy one that can.
|
|
340
342
|
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
* `prefixItems
|
|
344
|
-
*
|
|
345
|
-
*
|
|
343
|
+
* NO positional shape is a construct every wire accepts. That was measured the
|
|
344
|
+
* expensive way: `items: [a, b]` is refused by a wire validating against JSON
|
|
345
|
+
* Schema 2020-12, `prefixItems: [a, b]` is unknown to a draft-07 one — which
|
|
346
|
+
* DROPS it silently rather than complaining — and only three of the ten
|
|
347
|
+
* drivers convert between them. So the rendering boundary now collapses every
|
|
348
|
+
* tuple to a uniform array (`registry/tool/portable.ts`), and this function's
|
|
349
|
+
* job changed with it: the tuple it returns is the PARSER, which still refuses
|
|
350
|
+
* a wrongly-typed or wrongly-ordered call, while the positions reach the model
|
|
351
|
+
* as prose in the description — the one channel every wire carries intact.
|
|
352
|
+
*
|
|
353
|
+
* Hence the narrow gate, which still matters: a tuple is a parser strict
|
|
354
|
+
* enough to reject a shorter array, so it is built only where the server
|
|
355
|
+
* itself pinned the arity and closed the tail. Everything else keeps the
|
|
356
|
+
* permissive array. Either way the positional shape is described.
|
|
346
357
|
*
|
|
347
358
|
* The subtlety worth stating, because it inverts the intuition: positional
|
|
348
359
|
* `items`/`prefixItems` does not constrain LENGTH. Without `minItems` the
|
|
@@ -372,7 +383,12 @@ function positionalToZod(
|
|
|
372
383
|
const members = positional.map((member) => jsonSchemaPropertyToZod(member, depth + 1))
|
|
373
384
|
// Never `.rest()`. It renders a tail schema this wire has not been measured
|
|
374
385
|
// against, and the gate above has already established there is no tail.
|
|
375
|
-
|
|
386
|
+
//
|
|
387
|
+
// Described as well as parsed: the rendering collapses the tuple to a
|
|
388
|
+
// uniform array, so without this sentence the model would be shown "two of
|
|
389
|
+
// string-or-number" for a schema the parser reads as "a string then a
|
|
390
|
+
// number" — and would learn the difference only from a rejected call.
|
|
391
|
+
return z.tuple(members as [z.ZodType, ...z.ZodType[]]).describe(describePositional(positional))
|
|
376
392
|
}
|
|
377
393
|
|
|
378
394
|
/** The positional shape in prose, for the cases a tuple cannot carry. */
|
|
@@ -502,12 +518,68 @@ export function mcpToolToToolDefinition(
|
|
|
502
518
|
},
|
|
503
519
|
}
|
|
504
520
|
}
|
|
521
|
+
// MRTR: a conforming server never sends this, since namzu declares
|
|
522
|
+
// `clientCapabilities: {}` — but a non-conforming one, or a second
|
|
523
|
+
// `input_required` after the one automatic retry `MCPClient`
|
|
524
|
+
// already made, is named here rather than reaching the caller as
|
|
525
|
+
// an unexplained rejection. No side effect is known to have
|
|
526
|
+
// happened on this path (the spec's model is that a call ending
|
|
527
|
+
// in `input_required` has not truly run yet), so a retry — of
|
|
528
|
+
// this tool, or a different approach entirely — is safe.
|
|
529
|
+
if (error instanceof MCPInputRequiredError) {
|
|
530
|
+
const requested = error.inputRequests.map((request) => request.method)
|
|
531
|
+
return frameServerResult(
|
|
532
|
+
{
|
|
533
|
+
success: false,
|
|
534
|
+
output: '',
|
|
535
|
+
error: `MCP tool "${tool.name}" on server "${serverName}" asked for input this client has no way to supply${requested.length > 0 ? ` (${requested.join(', ')})` : ''}.`,
|
|
536
|
+
data: {
|
|
537
|
+
code: 'mcp_tool_input_required',
|
|
538
|
+
server: serverName,
|
|
539
|
+
tool: tool.name,
|
|
540
|
+
requested,
|
|
541
|
+
retrySafety: 'safe',
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
serverName,
|
|
545
|
+
tool.name,
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
// Same principle, for the other outcome a no-capability host is
|
|
549
|
+
// genuinely likely to see: the server refused before running the
|
|
550
|
+
// tool because this client never declared a capability it needs.
|
|
551
|
+
if (isMissingRequiredClientCapabilityError(error)) {
|
|
552
|
+
const requiredCapabilities = requiredCapabilitiesFrom(error.data)
|
|
553
|
+
return frameServerResult(
|
|
554
|
+
{
|
|
555
|
+
success: false,
|
|
556
|
+
output: '',
|
|
557
|
+
error: `MCP tool "${tool.name}" on server "${serverName}" requires client capabilities this client did not declare${requiredCapabilities.length > 0 ? ` (${requiredCapabilities.join(', ')})` : ''}.`,
|
|
558
|
+
data: {
|
|
559
|
+
code: 'mcp_tool_missing_client_capability',
|
|
560
|
+
server: serverName,
|
|
561
|
+
tool: tool.name,
|
|
562
|
+
requiredCapabilities,
|
|
563
|
+
retrySafety: 'safe',
|
|
564
|
+
},
|
|
565
|
+
},
|
|
566
|
+
serverName,
|
|
567
|
+
tool.name,
|
|
568
|
+
)
|
|
569
|
+
}
|
|
505
570
|
throw error
|
|
506
571
|
}
|
|
507
572
|
},
|
|
508
573
|
}
|
|
509
574
|
}
|
|
510
575
|
|
|
576
|
+
/** Read `-32021`'s `data.requiredCapabilities` defensively — it is a server's own claim, not a validated shape. */
|
|
577
|
+
function requiredCapabilitiesFrom(data: unknown): string[] {
|
|
578
|
+
if (typeof data !== 'object' || data === null) return []
|
|
579
|
+
const required = (data as { requiredCapabilities?: unknown }).requiredCapabilities
|
|
580
|
+
return Array.isArray(required) ? required.filter((c): c is string => typeof c === 'string') : []
|
|
581
|
+
}
|
|
582
|
+
|
|
511
583
|
export function toolDefinitionToMCPTool(tool: ToolDefinition): MCPToolDefinition {
|
|
512
584
|
return {
|
|
513
585
|
name: tool.name,
|
|
@@ -595,6 +667,11 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
595
667
|
block.type === 'image',
|
|
596
668
|
)
|
|
597
669
|
const imagesAdmitted = admitMcpImageBatch(imageBlocks)
|
|
670
|
+
const audioBlocks = result.content.filter(
|
|
671
|
+
(block): block is Extract<(typeof result.content)[number], { type: 'audio' }> =>
|
|
672
|
+
block.type === 'audio',
|
|
673
|
+
)
|
|
674
|
+
const audiosAdmitted = admitMcpAudioBatch(audioBlocks)
|
|
598
675
|
|
|
599
676
|
// Non-text blocks used to be filtered out and never seen again: a
|
|
600
677
|
// bridged MCP server returning a chart, a screenshot or a PDF had that
|
|
@@ -603,6 +680,13 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
603
680
|
// as model-visible content when any are present.
|
|
604
681
|
const blocks: ToolResultBlock[] = []
|
|
605
682
|
const pins: WorkingStatePin[] = []
|
|
683
|
+
// Set only by a block type that has no `ToolResultBlock` shape of its
|
|
684
|
+
// own and is rendered as a plain text pointer instead (`resource_link`
|
|
685
|
+
// today). Without this, a result made up entirely of such pointers
|
|
686
|
+
// would compute `hasRichContent` false — every pushed block LOOKS like
|
|
687
|
+
// ordinary text — and the whole `content` array, the pointer's only
|
|
688
|
+
// carrier, would be dropped by the `hasRichContent` gate below.
|
|
689
|
+
let hasNamedPointer = false
|
|
606
690
|
for (const block of result.content) {
|
|
607
691
|
if (block.type === 'resource' && block.resource?.mimeType === WORKING_STATE_MIME) {
|
|
608
692
|
// A server pins facts by returning them as a resource of this type:
|
|
@@ -626,14 +710,31 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
626
710
|
}
|
|
627
711
|
: {}),
|
|
628
712
|
})
|
|
713
|
+
} else if (block.type === 'resource_link') {
|
|
714
|
+
// A resource_link never carries content, admitted or not — it is
|
|
715
|
+
// a pointer the model cannot dereference, so it is named rather
|
|
716
|
+
// than pretended to be present, exactly like a URI-only embedded
|
|
717
|
+
// resource just below.
|
|
718
|
+
blocks.push({
|
|
719
|
+
type: 'text',
|
|
720
|
+
text: `[MCP resource link: ${block.name} (${block.uri})]`,
|
|
721
|
+
})
|
|
722
|
+
hasNamedPointer = true
|
|
629
723
|
} else if (block.type === 'resource' && block.resource?.text) {
|
|
630
724
|
// A resource with inline text is readable content; one that is
|
|
631
|
-
// only a URI is a pointer the model cannot
|
|
632
|
-
// named rather than pretended to be
|
|
725
|
+
// only a URI, or only a `blob`, is a pointer the model cannot
|
|
726
|
+
// dereference, so it is named rather than pretended to be
|
|
727
|
+
// present — here, that means it is left out of `blocks`
|
|
728
|
+
// entirely rather than rendered as fabricated text.
|
|
633
729
|
blocks.push({ type: 'text', text: block.resource.text })
|
|
634
730
|
}
|
|
731
|
+
// `audio` has no `ToolResultBlock` shape of its own today, so it is
|
|
732
|
+
// summarized into `output` below rather than pushed here — the
|
|
733
|
+
// same reasoning that keeps a withheld image out of plain text,
|
|
734
|
+
// applied to a media type this adapter cannot pass through as
|
|
735
|
+
// bytes at all yet.
|
|
635
736
|
}
|
|
636
|
-
const hasRichContent = blocks.some((b) => b.type !== 'text')
|
|
737
|
+
const hasRichContent = hasNamedPointer || blocks.some((b) => b.type !== 'text')
|
|
637
738
|
|
|
638
739
|
// A server may answer with a structured payload and skip the
|
|
639
740
|
// compatibility text block. Serializing it is the difference between
|
|
@@ -648,7 +749,19 @@ export function mcpToolResultToToolResult(result: MCPToolResult): ToolResult {
|
|
|
648
749
|
imageBlocks.length > 0 && !imagesAdmitted
|
|
649
750
|
? '[MCP image batch withheld from model input: one or more blocks are not complete supported raster containers matching their declared media types.]'
|
|
650
751
|
: ''
|
|
651
|
-
|
|
752
|
+
// Audio has no `ToolResultBlock` carrier, so an admitted clip is named
|
|
753
|
+
// in `output` by its media type instead of being dumped into `content`
|
|
754
|
+
// as base64 text — the same mistake this adapter already fixed once for
|
|
755
|
+
// screenshots. A withheld batch gets the same notice shape as images.
|
|
756
|
+
const audioNotice =
|
|
757
|
+
audioBlocks.length === 0
|
|
758
|
+
? ''
|
|
759
|
+
: audiosAdmitted
|
|
760
|
+
? `[MCP audio: ${audioBlocks.map((block) => block.mimeType).join(', ')}]`
|
|
761
|
+
: '[MCP audio batch withheld from model input: one or more blocks are not complete supported audio containers matching their declared media types.]'
|
|
762
|
+
const output = [visibleText, invalidImageNotice, audioNotice]
|
|
763
|
+
.filter((part) => part.length > 0)
|
|
764
|
+
.join('\n')
|
|
652
765
|
|
|
653
766
|
return {
|
|
654
767
|
...(pins.length > 0 ? { workingState: pins } : {}),
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { MCPContentBlock } from '../../types/connector/index.js'
|
|
2
|
+
|
|
3
|
+
const MAX_ENCODED_BYTES = 24 * 1024 * 1024
|
|
4
|
+
|
|
5
|
+
type MCPAudioBlock = Extract<MCPContentBlock, { type: 'audio' }>
|
|
6
|
+
type AudioMediaType = 'audio/wav' | 'audio/mpeg' | 'audio/ogg'
|
|
7
|
+
|
|
8
|
+
const AUDIO_MEDIA_TYPES = new Set<AudioMediaType>(['audio/wav', 'audio/mpeg', 'audio/ogg'])
|
|
9
|
+
|
|
10
|
+
function isAudioMediaType(value: string): value is AudioMediaType {
|
|
11
|
+
return AUDIO_MEDIA_TYPES.has(value as AudioMediaType)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function decodeCanonicalBase64(value: string): Uint8Array | null {
|
|
15
|
+
if (value.length === 0 || value.length > MAX_ENCODED_BYTES) return null
|
|
16
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
const decoded = Buffer.from(value, 'base64')
|
|
20
|
+
return decoded.toString('base64') === value ? decoded : null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A canonical RIFF/WAVE container: the size field agrees with the buffer
|
|
25
|
+
* length, and walking its chunks (word-aligned, as RIFF requires) lands
|
|
26
|
+
* exactly on the end of the buffer having seen both a `fmt ` and a `data`
|
|
27
|
+
* chunk. Mirrors the WebP walker in `image-admission.ts`, which is the same
|
|
28
|
+
* RIFF container format wearing a different fourCC.
|
|
29
|
+
*/
|
|
30
|
+
function isWav(bytes: Uint8Array): boolean {
|
|
31
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
32
|
+
if (
|
|
33
|
+
bytes.length < 12 ||
|
|
34
|
+
buffer.toString('ascii', 0, 4) !== 'RIFF' ||
|
|
35
|
+
buffer.readUInt32LE(4) + 8 !== bytes.length ||
|
|
36
|
+
buffer.toString('ascii', 8, 12) !== 'WAVE'
|
|
37
|
+
) {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
let offset = 12
|
|
41
|
+
let sawFmt = false
|
|
42
|
+
let sawData = false
|
|
43
|
+
while (offset + 8 <= bytes.length) {
|
|
44
|
+
const chunkId = buffer.toString('ascii', offset, offset + 4)
|
|
45
|
+
const chunkSize = buffer.readUInt32LE(offset + 4)
|
|
46
|
+
const dataStart = offset + 8
|
|
47
|
+
const dataEnd = dataStart + chunkSize
|
|
48
|
+
if (dataEnd < dataStart || dataEnd > bytes.length) return false
|
|
49
|
+
if (chunkId === 'fmt ') {
|
|
50
|
+
if (chunkSize < 16) return false
|
|
51
|
+
sawFmt = true
|
|
52
|
+
}
|
|
53
|
+
if (chunkId === 'data') sawData = true
|
|
54
|
+
// RIFF chunks are padded to an even byte count; the pad byte is not
|
|
55
|
+
// counted in chunkSize but is present in the stream.
|
|
56
|
+
offset = dataEnd + (chunkSize % 2)
|
|
57
|
+
}
|
|
58
|
+
return sawFmt && sawData && offset === bytes.length
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A sequence of Ogg pages, each framed by its `OggS` capture pattern and a
|
|
63
|
+
* segment table whose declared sizes account for every byte of payload.
|
|
64
|
+
* Page checksums are not verified — the framing itself, consumed exactly to
|
|
65
|
+
* the end of the buffer, is what this admits or refuses.
|
|
66
|
+
*/
|
|
67
|
+
function isOgg(bytes: Uint8Array): boolean {
|
|
68
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
69
|
+
let offset = 0
|
|
70
|
+
let sawPage = false
|
|
71
|
+
while (offset + 27 <= bytes.length) {
|
|
72
|
+
if (buffer.toString('ascii', offset, offset + 4) !== 'OggS') return false
|
|
73
|
+
if (bytes[offset + 4] !== 0) return false // stream_structure_version
|
|
74
|
+
const segmentCount = bytes[offset + 26] ?? 0
|
|
75
|
+
const segmentTableStart = offset + 27
|
|
76
|
+
if (segmentTableStart + segmentCount > bytes.length) return false
|
|
77
|
+
let payloadLength = 0
|
|
78
|
+
for (let i = 0; i < segmentCount; i += 1) {
|
|
79
|
+
payloadLength += bytes[segmentTableStart + i] ?? 0
|
|
80
|
+
}
|
|
81
|
+
const payloadEnd = segmentTableStart + segmentCount + payloadLength
|
|
82
|
+
if (payloadEnd > bytes.length) return false
|
|
83
|
+
sawPage = true
|
|
84
|
+
offset = payloadEnd
|
|
85
|
+
}
|
|
86
|
+
return sawPage && offset === bytes.length
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MPEG-1 Layer III bitrates in kbps, indexed by the header's 4-bit field.
|
|
90
|
+
// Index 0 (free bitrate) and 15 (reserved) are both treated as unsupported.
|
|
91
|
+
const MPEG1_L3_BITRATES: readonly number[] = [
|
|
92
|
+
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
|
|
93
|
+
]
|
|
94
|
+
const MPEG1_SAMPLE_RATES: readonly number[] = [44100, 48000, 32000, 0]
|
|
95
|
+
|
|
96
|
+
/** Frame length in bytes for an MPEG-1 Layer III frame, or null if the header is malformed/unsupported. */
|
|
97
|
+
function mp3FrameLength(headerByte1: number, headerByte2: number): number | null {
|
|
98
|
+
const versionBits = (headerByte1 >> 3) & 0x3
|
|
99
|
+
const layerBits = (headerByte1 >> 1) & 0x3
|
|
100
|
+
if (versionBits !== 0x3 || layerBits !== 0x1) return null // MPEG-1, Layer III only
|
|
101
|
+
const bitrateIndex = (headerByte2 >> 4) & 0xf
|
|
102
|
+
const sampleRateIndex = (headerByte2 >> 2) & 0x3
|
|
103
|
+
const padding = (headerByte2 >> 1) & 0x1
|
|
104
|
+
if (bitrateIndex === 0 || bitrateIndex === 0xf || sampleRateIndex === 0x3) return null
|
|
105
|
+
const bitrate = MPEG1_L3_BITRATES[bitrateIndex]
|
|
106
|
+
const sampleRate = MPEG1_SAMPLE_RATES[sampleRateIndex]
|
|
107
|
+
if (!bitrate || !sampleRate) return null
|
|
108
|
+
return Math.floor((144 * bitrate * 1000) / sampleRate) + padding
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function id3v2TagLength(bytes: Uint8Array, buffer: Buffer): number {
|
|
112
|
+
if (bytes.length < 10 || buffer.toString('ascii', 0, 3) !== 'ID3') return 0
|
|
113
|
+
// Synchsafe integer: 7 significant bits per byte, big-endian.
|
|
114
|
+
const size =
|
|
115
|
+
((bytes[6] ?? 0) & 0x7f) * 0x200000 +
|
|
116
|
+
((bytes[7] ?? 0) & 0x7f) * 0x4000 +
|
|
117
|
+
((bytes[8] ?? 0) & 0x7f) * 0x80 +
|
|
118
|
+
((bytes[9] ?? 0) & 0x7f)
|
|
119
|
+
return 10 + size
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* MPEG-1 Layer III frames, one after another with no trailing bytes: an
|
|
124
|
+
* optional ID3v2 tag is skipped, then every remaining byte must belong to a
|
|
125
|
+
* syntactically valid frame header whose declared length reaches exactly
|
|
126
|
+
* the next frame (or the end of the buffer). Other MPEG versions and layers
|
|
127
|
+
* are refused rather than approximated.
|
|
128
|
+
*/
|
|
129
|
+
function isMp3(bytes: Uint8Array): boolean {
|
|
130
|
+
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
131
|
+
const offsetAfterTag = id3v2TagLength(bytes, buffer)
|
|
132
|
+
if (offsetAfterTag + 4 > bytes.length) return false
|
|
133
|
+
let offset = offsetAfterTag
|
|
134
|
+
let sawFrame = false
|
|
135
|
+
while (offset + 4 <= bytes.length) {
|
|
136
|
+
if (bytes[offset] !== 0xff || ((bytes[offset + 1] ?? 0) & 0xe0) !== 0xe0) return false
|
|
137
|
+
const frameLength = mp3FrameLength(bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0)
|
|
138
|
+
if (frameLength === null || frameLength < 4 || offset + frameLength > bytes.length) {
|
|
139
|
+
return false
|
|
140
|
+
}
|
|
141
|
+
sawFrame = true
|
|
142
|
+
offset += frameLength
|
|
143
|
+
}
|
|
144
|
+
return sawFrame && offset === bytes.length
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function audioIsAdmitted(block: MCPAudioBlock): boolean {
|
|
148
|
+
if (!isAudioMediaType(block.mimeType)) return false
|
|
149
|
+
const bytes = decodeCanonicalBase64(block.data)
|
|
150
|
+
if (!bytes) return false
|
|
151
|
+
switch (block.mimeType) {
|
|
152
|
+
case 'audio/wav':
|
|
153
|
+
return isWav(bytes)
|
|
154
|
+
case 'audio/ogg':
|
|
155
|
+
return isOgg(bytes)
|
|
156
|
+
case 'audio/mpeg':
|
|
157
|
+
return isMp3(bytes)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Admit an MCP audio batch atomically before any member becomes model input.
|
|
163
|
+
*
|
|
164
|
+
* Mirrors `admitMcpImageBatch`: a remote server controls the declared MIME
|
|
165
|
+
* and bytes, canonical base64 is only an encoding property, and the decoded
|
|
166
|
+
* value must also be a complete, bounded container whose framing agrees
|
|
167
|
+
* with the declared media type. One bad member withholds the complete batch
|
|
168
|
+
* so a model is never shown a partial result while the host retains every
|
|
169
|
+
* raw block for inspection.
|
|
170
|
+
*/
|
|
171
|
+
export function admitMcpAudioBatch(audios: readonly MCPAudioBlock[]): boolean {
|
|
172
|
+
return audios.length === 0 || audios.every(audioIsAdmitted)
|
|
173
|
+
}
|