@animalabs/agent-framework 0.27.0 → 0.29.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/README.md +145 -41
- package/dist/src/agent.d.ts +29 -2
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +73 -12
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +3 -12
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/api/server.d.ts.map +1 -1
- package/dist/src/api/server.js +11 -5
- package/dist/src/api/server.js.map +1 -1
- package/dist/src/auto-continue.d.ts +5 -2
- package/dist/src/auto-continue.d.ts.map +1 -1
- package/dist/src/auto-continue.js +9 -5
- package/dist/src/auto-continue.js.map +1 -1
- package/dist/src/code-execution/isolate/runner.d.ts +7 -5
- package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/runner.js +7 -5
- package/dist/src/code-execution/isolate/runner.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +3 -2
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/cut-off-round.d.ts +24 -0
- package/dist/src/cut-off-round.d.ts.map +1 -0
- package/dist/src/cut-off-round.js +87 -0
- package/dist/src/cut-off-round.js.map +1 -0
- package/dist/src/framework.d.ts +468 -125
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +3651 -1111
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +133 -13
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +654 -59
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/index.d.ts +3 -2
- package/dist/src/gate/index.d.ts.map +1 -1
- package/dist/src/gate/index.js +2 -1
- package/dist/src/gate/index.js.map +1 -1
- package/dist/src/gate/reply-judge.d.ts +116 -0
- package/dist/src/gate/reply-judge.d.ts.map +1 -0
- package/dist/src/gate/reply-judge.js +467 -0
- package/dist/src/gate/reply-judge.js.map +1 -0
- package/dist/src/gate/types.d.ts +173 -2
- package/dist/src/gate/types.d.ts.map +1 -1
- package/dist/src/heartbeat/heartbeat.d.ts +202 -0
- package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
- package/dist/src/heartbeat/heartbeat.js +778 -0
- package/dist/src/heartbeat/heartbeat.js.map +1 -0
- package/dist/src/heartbeat/reminder-time.d.ts +5 -0
- package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
- package/dist/src/heartbeat/reminder-time.js +107 -0
- package/dist/src/heartbeat/reminder-time.js.map +1 -0
- package/dist/src/held-writes.d.ts +3 -3
- package/dist/src/held-writes.js +3 -3
- package/dist/src/host-presentation.d.ts +170 -0
- package/dist/src/host-presentation.d.ts.map +1 -0
- package/dist/src/host-presentation.js +561 -0
- package/dist/src/host-presentation.js.map +1 -0
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/line-reader.d.ts +4 -0
- package/dist/src/line-reader.d.ts.map +1 -0
- package/dist/src/line-reader.js +59 -0
- package/dist/src/line-reader.js.map +1 -0
- package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
- package/dist/src/mcpl/capability-mask.js +3 -2
- package/dist/src/mcpl/capability-mask.js.map +1 -1
- package/dist/src/mcpl/channel-registry.d.ts +34 -6
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +133 -54
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
- package/dist/src/mcpl/conversation-router.js +2 -1
- package/dist/src/mcpl/conversation-router.js.map +1 -1
- package/dist/src/mcpl/convert-block.d.ts +1 -0
- package/dist/src/mcpl/convert-block.d.ts.map +1 -1
- package/dist/src/mcpl/convert-block.js +16 -4
- package/dist/src/mcpl/convert-block.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +4 -3
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +29 -11
- package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
- package/dist/src/mcpl/inbound-source.js +43 -70
- package/dist/src/mcpl/inbound-source.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +11 -7
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +3 -2
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +25 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-handler.js +2 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +12 -40
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +29 -23
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +18 -8
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +70 -30
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
- package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
- package/dist/src/mcpl/skip-reply-wake.js +25 -0
- package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
- package/dist/src/mcpl/tags.d.ts +9 -2
- package/dist/src/mcpl/tags.d.ts.map +1 -1
- package/dist/src/mcpl/tags.js +12 -2
- package/dist/src/mcpl/tags.js.map +1 -1
- package/dist/src/mcpl/tool-presence.d.ts +66 -14
- package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
- package/dist/src/mcpl/tool-presence.js +150 -80
- package/dist/src/mcpl/tool-presence.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +10 -3
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +60 -30
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/toolset-host.d.ts +88 -55
- package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
- package/dist/src/mcpl/toolset-host.js +220 -111
- package/dist/src/mcpl/toolset-host.js.map +1 -1
- package/dist/src/mcpl/toolsets-module.d.ts +27 -35
- package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
- package/dist/src/mcpl/toolsets-module.js +313 -91
- package/dist/src/mcpl/toolsets-module.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +1 -1
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +16 -16
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +31 -13
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +10 -8
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +15 -9
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/api/index.d.ts.map +1 -1
- package/dist/src/modules/api/index.js +4 -2
- package/dist/src/modules/api/index.js.map +1 -1
- package/dist/src/modules/discord/index.d.ts.map +1 -1
- package/dist/src/modules/discord/index.js +33 -25
- package/dist/src/modules/discord/index.js.map +1 -1
- package/dist/src/modules/history/index.d.ts.map +1 -1
- package/dist/src/modules/history/index.js +30 -17
- package/dist/src/modules/history/index.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +7 -0
- package/dist/src/modules/sender-name.d.ts.map +1 -0
- package/dist/src/modules/sender-name.js +10 -0
- package/dist/src/modules/sender-name.js.map +1 -0
- package/dist/src/modules/workspace/answers.d.ts +26 -0
- package/dist/src/modules/workspace/answers.d.ts.map +1 -0
- package/dist/src/modules/workspace/answers.js +138 -0
- package/dist/src/modules/workspace/answers.js.map +1 -0
- package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
- package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
- package/dist/src/modules/workspace/branch-intent.js +6 -2
- package/dist/src/modules/workspace/branch-intent.js.map +1 -1
- package/dist/src/modules/workspace/direct-write.d.ts +3 -0
- package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
- package/dist/src/modules/workspace/direct-write.js +59 -24
- package/dist/src/modules/workspace/direct-write.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +48 -17
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/edit-text.d.ts +2 -1
- package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/edit-text.js +4 -2
- package/dist/src/modules/workspace/edit-text.js.map +1 -1
- package/dist/src/modules/workspace/effects.d.ts +4 -2
- package/dist/src/modules/workspace/effects.d.ts.map +1 -1
- package/dist/src/modules/workspace/effects.js +67 -50
- package/dist/src/modules/workspace/effects.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +63 -51
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +980 -547
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +125 -82
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +465 -241
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
- package/dist/src/modules/workspace/read-page.js +7 -1
- package/dist/src/modules/workspace/read-page.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +7 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +156 -50
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/result-text.d.ts +1 -0
- package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
- package/dist/src/modules/workspace/result-text.js.map +1 -1
- package/dist/src/modules/workspace/sync.d.ts +9 -0
- package/dist/src/modules/workspace/sync.d.ts.map +1 -1
- package/dist/src/modules/workspace/sync.js +14 -0
- package/dist/src/modules/workspace/sync.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +96 -43
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +8 -3
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/notice-tags.d.ts +79 -0
- package/dist/src/notice-tags.d.ts.map +1 -0
- package/dist/src/notice-tags.js +83 -0
- package/dist/src/notice-tags.js.map +1 -0
- package/dist/src/prose-drafts.d.ts +4 -3
- package/dist/src/prose-drafts.d.ts.map +1 -1
- package/dist/src/prose-drafts.js +3 -2
- package/dist/src/prose-drafts.js.map +1 -1
- package/dist/src/reasoning-loss-notice.d.ts +23 -0
- package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
- package/dist/src/reasoning-loss-notice.js +52 -0
- package/dist/src/reasoning-loss-notice.js.map +1 -0
- package/dist/src/reasoning-loss.d.ts +24 -0
- package/dist/src/reasoning-loss.d.ts.map +1 -0
- package/dist/src/reasoning-loss.js +102 -0
- package/dist/src/reasoning-loss.js.map +1 -0
- package/dist/src/send-tools.d.ts +3 -2
- package/dist/src/send-tools.d.ts.map +1 -1
- package/dist/src/send-tools.js +3 -2
- package/dist/src/send-tools.js.map +1 -1
- package/dist/src/sent-rows.js +1 -1
- package/dist/src/silent-heartbeat.d.ts +1 -53
- package/dist/src/silent-heartbeat.d.ts.map +1 -1
- package/dist/src/silent-heartbeat.js +56 -3
- package/dist/src/silent-heartbeat.js.map +1 -1
- package/dist/src/supplied-text.d.ts +35 -0
- package/dist/src/supplied-text.d.ts.map +1 -0
- package/dist/src/supplied-text.js +141 -0
- package/dist/src/supplied-text.js.map +1 -0
- package/dist/src/thinking-drops.d.ts +4 -11
- package/dist/src/thinking-drops.d.ts.map +1 -1
- package/dist/src/thinking-drops.js +7 -14
- package/dist/src/thinking-drops.js.map +1 -1
- package/dist/src/tool-args.d.ts +12 -0
- package/dist/src/tool-args.d.ts.map +1 -1
- package/dist/src/tool-args.js +39 -4
- package/dist/src/tool-args.js.map +1 -1
- package/dist/src/tool-image-ledger.d.ts.map +1 -1
- package/dist/src/tool-image-ledger.js +2 -1
- package/dist/src/tool-image-ledger.js.map +1 -1
- package/dist/src/tool-presentation.d.ts.map +1 -1
- package/dist/src/tool-presentation.js +5 -4
- package/dist/src/tool-presentation.js.map +1 -1
- package/dist/src/tool-result-guard.d.ts +8 -4
- package/dist/src/tool-result-guard.d.ts.map +1 -1
- package/dist/src/tool-result-guard.js +23 -11
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +3 -2
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +24 -26
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +4 -1
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/types/agent.d.ts +63 -18
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +18 -8
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +58 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +27 -13
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +34 -1
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +76 -12
- package/src/api/mcp-server-core.ts +3 -14
- package/src/api/server.ts +11 -5
- package/src/auto-continue.ts +12 -5
- package/src/code-execution/isolate/runner.ts +7 -5
- package/src/code-execution/isolate/scripts.ts +3 -2
- package/src/cut-off-round.ts +98 -0
- package/src/framework.ts +3772 -1138
- package/src/gate/event-gate.ts +705 -60
- package/src/gate/index.ts +9 -1
- package/src/gate/reply-judge.ts +512 -0
- package/src/gate/types.ts +176 -3
- package/src/heartbeat/heartbeat.ts +843 -0
- package/src/heartbeat/reminder-time.ts +107 -0
- package/src/held-writes.ts +3 -3
- package/src/host-presentation.ts +605 -0
- package/src/index.ts +5 -1
- package/src/line-reader.ts +58 -0
- package/src/mcpl/capability-mask.ts +3 -2
- package/src/mcpl/channel-registry.ts +139 -53
- package/src/mcpl/conversation-router.ts +2 -1
- package/src/mcpl/convert-block.ts +17 -4
- package/src/mcpl/hook-orchestrator.ts +4 -3
- package/src/mcpl/inbound-source.ts +45 -70
- package/src/mcpl/modern-connection.ts +11 -7
- package/src/mcpl/protocol-family.ts +24 -3
- package/src/mcpl/push-handler.ts +2 -2
- package/src/mcpl/references.ts +31 -23
- package/src/mcpl/server-connection.ts +76 -34
- package/src/mcpl/skip-reply-wake.ts +35 -0
- package/src/mcpl/tags.ts +13 -2
- package/src/mcpl/tool-presence.ts +149 -82
- package/src/mcpl/tool-signature.ts +64 -32
- package/src/mcpl/toolset-host.ts +212 -110
- package/src/mcpl/toolsets-module.ts +277 -91
- package/src/mcpl/transport.ts +18 -14
- package/src/mcpl/types.ts +32 -13
- package/src/module-registry.ts +14 -9
- package/src/modules/api/index.ts +4 -2
- package/src/modules/discord/index.ts +37 -25
- package/src/modules/history/index.ts +30 -18
- package/src/modules/sender-name.ts +9 -0
- package/src/modules/workspace/answers.ts +151 -0
- package/src/modules/workspace/branch-intent.ts +10 -5
- package/src/modules/workspace/direct-write.ts +59 -23
- package/src/modules/workspace/disk-agreement.ts +52 -21
- package/src/modules/workspace/edit-text.ts +7 -5
- package/src/modules/workspace/effects.ts +69 -50
- package/src/modules/workspace/index.ts +966 -556
- package/src/modules/workspace/observe.ts +497 -242
- package/src/modules/workspace/read-page.ts +7 -1
- package/src/modules/workspace/reconcile.ts +184 -49
- package/src/modules/workspace/result-text.ts +2 -0
- package/src/modules/workspace/sync.ts +16 -0
- package/src/modules/workspace/types.ts +96 -43
- package/src/modules/workspace/watcher.ts +8 -3
- package/src/notice-tags.ts +84 -0
- package/src/prose-drafts.ts +4 -3
- package/src/reasoning-loss-notice.ts +64 -0
- package/src/reasoning-loss.ts +120 -0
- package/src/send-tools.ts +3 -2
- package/src/sent-rows.ts +1 -1
- package/src/silent-heartbeat.ts +4 -3
- package/src/supplied-text.ts +154 -0
- package/src/thinking-drops.ts +10 -16
- package/src/tool-args.ts +38 -4
- package/src/tool-image-ledger.ts +2 -1
- package/src/tool-presentation.ts +5 -4
- package/src/tool-result-guard.ts +26 -11
- package/src/tune-out/coordinator.ts +25 -27
- package/src/tune-out/tools.ts +4 -1
- package/src/types/agent.ts +63 -18
- package/src/types/events.ts +18 -8
- package/src/types/framework.ts +59 -0
- package/src/types/module.ts +28 -13
- package/src/types/trace.ts +36 -1
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
|
+
import { blockAsStored, MalformedContentError } from '@animalabs/context-manager';
|
|
26
|
+
import { logDetail, logValue } from './log-value.js';
|
|
25
27
|
|
|
26
28
|
/** The MCPL admission lane that accepted an item. Its contract decides what
|
|
27
29
|
* an `eventId` is worth: `push/event` deduplicates by eventId, and RFC-006
|
|
@@ -192,92 +194,65 @@ export function canonicalJson(value: unknown): string {
|
|
|
192
194
|
}
|
|
193
195
|
|
|
194
196
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* 'base64', data, mediaType } }` and nothing else, its data re-encoded from
|
|
199
|
-
* the decoded bytes, and an image's media type taken from its bytes'
|
|
200
|
-
* signature where they have one. Any other block comes back as it was given,
|
|
201
|
-
* except a store-internal `blob_ref` from an unresolved read, which resolves
|
|
202
|
-
* to inline media by default. It is digested as given here, so an unresolved
|
|
203
|
-
* read and its resolved form differ. Context-manager refuses caller-supplied
|
|
204
|
-
* blob_ref writes; the MCPL lanes never store one.
|
|
197
|
+
* blockAsStored throws a `MalformedContentError` for a block the store
|
|
198
|
+
* refuses on write, a caller-supplied `blob_ref` among them. Such a block
|
|
199
|
+
* has no stored form, and whatever is thrown, it is digested as given.
|
|
205
200
|
*/
|
|
206
201
|
function storedBlock(block: unknown): unknown {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Base64 as the store writes it back from the decoded bytes: padded, with no
|
|
222
|
-
* whitespace, the standard alphabet and zero unused bits. Data already in
|
|
223
|
-
* that form, as every stored copy's is, is returned without decoding it all.
|
|
224
|
-
*/
|
|
225
|
-
function canonicalBase64(data: string): string {
|
|
226
|
-
if (data.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(data)) {
|
|
227
|
-
const last = data.slice(-4);
|
|
228
|
-
if (Buffer.from(last, 'base64').toString('base64') === last) return data;
|
|
229
|
-
}
|
|
230
|
-
return Buffer.from(data, 'base64').toString('base64');
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* The store's own test for raster image types with an unambiguous byte
|
|
235
|
-
* signature, copied from context-manager's blob-manager.ts (it isn't
|
|
236
|
-
* exported): the store relabels an image whose bytes match one of these.
|
|
237
|
-
*/
|
|
238
|
-
function sniffRasterImageMediaType(bytes: Uint8Array): string | null {
|
|
239
|
-
if (bytes.length >= 8 &&
|
|
240
|
-
bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
|
|
241
|
-
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) {
|
|
242
|
-
return 'image/png';
|
|
243
|
-
}
|
|
244
|
-
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) {
|
|
245
|
-
return 'image/jpeg';
|
|
246
|
-
}
|
|
247
|
-
if (bytes.length >= 6) {
|
|
248
|
-
const signature = Buffer.from(bytes.subarray(0, 6)).toString('ascii');
|
|
249
|
-
if (signature === 'GIF87a' || signature === 'GIF89a') return 'image/gif';
|
|
250
|
-
}
|
|
251
|
-
if (bytes.length >= 12 &&
|
|
252
|
-
Buffer.from(bytes.subarray(0, 4)).toString('ascii') === 'RIFF' &&
|
|
253
|
-
Buffer.from(bytes.subarray(8, 12)).toString('ascii') === 'WEBP') {
|
|
254
|
-
return 'image/webp';
|
|
202
|
+
try {
|
|
203
|
+
return blockAsStored(block as Parameters<typeof blockAsStored>[0]);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (!(err instanceof MalformedContentError)) {
|
|
206
|
+
const { name, message } = (err ?? {}) as { name?: unknown; message?: unknown };
|
|
207
|
+
console.error(
|
|
208
|
+
`[stored-digest-as-given] error=${logValue(typeof name === 'string' ? name : typeof err)}: ` +
|
|
209
|
+
logDetail(typeof message === 'string' ? message : ''),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return block;
|
|
255
213
|
}
|
|
256
|
-
return null;
|
|
257
214
|
}
|
|
258
215
|
|
|
259
216
|
/**
|
|
260
217
|
* The version identity of a delivered body (agreed with the receipts lane,
|
|
261
218
|
* room-220 #46752–#47210): SHA-256 hex of `canonicalJson([blocks])` over the
|
|
262
219
|
* ContentBlock[] the framework stores for the body — after MCPL conversion,
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
220
|
+
* which marks the server's text at the framework's notice openings
|
|
221
|
+
* (supplied-text.ts), before any host decoration is added and before storage
|
|
222
|
+
* shards it — with
|
|
223
|
+
* each block as the store keeps it, which context-manager's `blockAsStored`
|
|
224
|
+
* gives. So a body hashes alike as delivered, as handed to storage, and as
|
|
225
|
+
* read back unsharded from the store with its blobs resolved, except one
|
|
226
|
+
* whose blocks the store changes together: when it mends a `tool_use` whose
|
|
227
|
+
* input isn't an object, it also releases a raw replay form (`rawXml`,
|
|
228
|
+
* `rawItem`) that the message's other blocks share, which no one block's
|
|
229
|
+
* stored form shows. A body an MCPL lane stamps is never one. A block the
|
|
230
|
+
* store refuses on write, a `blob_ref` among them, is digested as given. The
|
|
231
|
+
* one-element array keeps the framing an undecorated, unsharded stored copy
|
|
232
|
+
* has always hashed to, so a stamped copy and an older copy of the same body
|
|
233
|
+
* share a version where the store keeps the body as it did then.
|
|
234
|
+
*
|
|
235
|
+
* A digest is a digest under the context-manager release that computed it: a
|
|
236
|
+
* release that changes what the store keeps, such as its set of image
|
|
237
|
+
* signatures, changes the digest of a body it touches.
|
|
269
238
|
*
|
|
270
239
|
* Ingestion stamps it as `metadata.sourceBodyDigest`, and the same function
|
|
271
240
|
* over exactly the blocks handed to storage (decorations included) as
|
|
272
241
|
* `metadata.storedBodyDigest`, on both MCPL lanes. Both live outside the
|
|
273
242
|
* frozen admission envelope: they describe the body actually delivered (a
|
|
274
243
|
* materialization, a correction), not the admission. Neither proves later
|
|
275
|
-
* presence or completeness
|
|
244
|
+
* presence or completeness. An unsharded stored copy that no longer hashes
|
|
276
245
|
* to its storedBodyDigest was changed after delivery (editMessage keeps
|
|
277
|
-
* metadata)
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
246
|
+
* metadata), or holds blocks the store changes together, or its digest was
|
|
247
|
+
* taken under a release that kept some block of it differently: a
|
|
248
|
+
* context-manager release that changed a stored form, or an agent-framework
|
|
249
|
+
* release from 0.24.0 up to the one that brought in blockAsStored, which
|
|
250
|
+
* took stored forms from a copy of the store's transform. Digests already
|
|
251
|
+
* stamped are never recomputed. A body the store shards carries the whole
|
|
252
|
+
* body's two digests on every shard, and no shard hashes to them. Check a
|
|
253
|
+
* copy read back with its blobs resolved (getAllMessages, getMessage, or
|
|
254
|
+
* getMessageWindow without `resolveBlobs: false`): a blob reference never
|
|
255
|
+
* matches.
|
|
281
256
|
*
|
|
282
257
|
* Exported from the package root, so a consumer checks a copy against either
|
|
283
258
|
* field with this same function rather than a copy of it.
|
|
@@ -68,7 +68,7 @@ import {
|
|
|
68
68
|
|
|
69
69
|
import { StdioTransport, type McplTransport } from './transport.js';
|
|
70
70
|
import { McplProtocolVersionError, McplRequestError, McplUnreapedLaunchError } from './server-connection.js';
|
|
71
|
-
import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
|
|
71
|
+
import { LEGACY_MCP_PROTOCOL_VERSION, MAX_TIMER_MS, MODERN_MCP_PROTOCOL_VERSION, checkServerConfig, describeCredentialError, logDroppedLine, maskedError, noteDialSent, redactServerText, redactedData, serverTextRedactor, type ServerTextRedactor } from './protocol-family.js';
|
|
72
72
|
import type { McplServerConfig, McpToolDefinition } from './types.js';
|
|
73
73
|
import { boundedForLog } from './errors.js';
|
|
74
74
|
import { logDetail, logValue, logValueRedacted, loggedCode } from './log-value.js';
|
|
@@ -297,8 +297,9 @@ export function malformedLineDiagnostic(
|
|
|
297
297
|
|
|
298
298
|
/**
|
|
299
299
|
* SDK `Transport` over Connectome's stdio spawner. Framing is the same
|
|
300
|
-
* newline-delimited JSON-RPC as legacy. Lines that are not JSON are
|
|
301
|
-
*
|
|
300
|
+
* newline-delimited JSON-RPC as legacy. Lines that are not JSON are dropped,
|
|
301
|
+
* and the first three on a connection that aren't blank are logged by their
|
|
302
|
+
* length, as the legacy engine does. Because this is a custom transport, the
|
|
302
303
|
* SDK runs `server/discover` on this very process instead of starting a
|
|
303
304
|
* throwaway copy, and cancels with `notifications/cancelled`, which is the
|
|
304
305
|
* stdio binding's mechanism.
|
|
@@ -332,14 +333,17 @@ class SpawnerStdioTransport implements Transport {
|
|
|
332
333
|
const line = await this.spawning;
|
|
333
334
|
this.line = line;
|
|
334
335
|
line.on('line', (text: string) => {
|
|
335
|
-
// A line that isn't JSON is
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
336
|
+
// A line that isn't JSON is dropped, and the first three on a
|
|
337
|
+
// connection that aren't blank are logged by their length, as the
|
|
338
|
+
// legacy engine does. A line that is JSON but not a JSON-RPC message
|
|
339
|
+
// (`null`, an array, a bare value) is a diagnostic. It must never reach
|
|
340
|
+
// a handler that assumes the shape: thrown from this callback, it would
|
|
341
|
+
// end the host.
|
|
339
342
|
let parsed: unknown;
|
|
340
343
|
try {
|
|
341
344
|
parsed = JSON.parse(text);
|
|
342
345
|
} catch {
|
|
346
|
+
logDroppedLine(this.config.id, text, 'unparsed', line);
|
|
343
347
|
return;
|
|
344
348
|
}
|
|
345
349
|
let message: JSONRPCMessage;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* isn't `initialize`), the SDK's stdio probe launches the server twice, and a
|
|
23
23
|
* server's era is a stable fact its configuration can state.
|
|
24
24
|
*/
|
|
25
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
25
26
|
import { DENY_BY_DEFAULT, capabilityPatternMatches } from './capability-grant.js';
|
|
26
27
|
import { DEFAULT_EAGER_FETCH_MAX_BYTES, DEFAULT_MAX_TOTAL_BYTES } from './reference-fetcher.js';
|
|
27
28
|
import { DEFAULT_MAX_INPUT_BYTES } from './tool-lifecycle.js';
|
|
@@ -198,11 +199,11 @@ export function serverTextRedactor(config: Pick<McplServerConfig, 'url' | 'token
|
|
|
198
199
|
|
|
199
200
|
/** What a redaction gives in place of text when the configuration it
|
|
200
201
|
* redacts with can't be read ({@link redactServerText}). */
|
|
201
|
-
export const WITHHELD_TEXT =
|
|
202
|
+
export const WITHHELD_TEXT = `[${NOTICE_TAG.withheld}: the server's configuration couldn't be read to redact this]`;
|
|
202
203
|
|
|
203
204
|
/** What {@link redactedData} gives in place of data nested deeper than it
|
|
204
205
|
* walks. */
|
|
205
|
-
export const TOO_DEEP_TEXT =
|
|
206
|
+
export const TOO_DEEP_TEXT = `[${NOTICE_TAG.withheld}: nested too deep to redact]`;
|
|
206
207
|
|
|
207
208
|
/** How deep {@link redactedData} walks. A server's JSON can nest deeper than
|
|
208
209
|
* a recursive walk's stack holds (10,000 levels overflow it), and the walk
|
|
@@ -528,7 +529,7 @@ export function credentialRedactor(patterns: readonly CredentialPattern[]): Reda
|
|
|
528
529
|
/** Any value as text: a value that can't be printed reads as a placeholder. */
|
|
529
530
|
export function asText(text: unknown): string {
|
|
530
531
|
if (typeof text === 'string') return text;
|
|
531
|
-
try { return String(text); } catch { return
|
|
532
|
+
try { return String(text); } catch { return `[${NOTICE_TAG.unprintableValue}]`; }
|
|
532
533
|
}
|
|
533
534
|
|
|
534
535
|
/** A part of an environment variable's name that ends with a credential word, in the singular or plural, with any digits after it. */
|
|
@@ -869,3 +870,23 @@ export function checkServerConfig(config: McplServerConfig): ServerBinding {
|
|
|
869
870
|
for (const warning of serverConfigWarnings(config)) console.warn(`[mcp] ${warning}`);
|
|
870
871
|
return resolveServerBinding(config);
|
|
871
872
|
}
|
|
873
|
+
|
|
874
|
+
const DROPPED_LINES_LOGGED = 3;
|
|
875
|
+
const droppedLinesLogged = new WeakMap<object, number>();
|
|
876
|
+
|
|
877
|
+
export function logDroppedLine(serverId: string, line: string, reason: 'unparsed' | 'not-object', connection: object): void {
|
|
878
|
+
if (/^[ \t]*$/.test(line)) return;
|
|
879
|
+
const logged = droppedLinesLogged.get(connection) ?? 0;
|
|
880
|
+
if (logged > DROPPED_LINES_LOGGED) return;
|
|
881
|
+
droppedLinesLogged.set(connection, logged + 1);
|
|
882
|
+
if (logged === DROPPED_LINES_LOGGED) {
|
|
883
|
+
console.error(`MCP server "${serverId}" sent another line that isn't a JSON object, so it was dropped; more such lines on this connection are dropped without a log line`);
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
let length = 0;
|
|
887
|
+
for (const _ of line) length++;
|
|
888
|
+
const why = reason === 'not-object' ? 'is JSON but not an object'
|
|
889
|
+
: /^[ \t]*\{/.test(line) ? "starts like a JSON object but doesn't parse"
|
|
890
|
+
: "isn't JSON";
|
|
891
|
+
console.error(`MCP server "${serverId}" sent a line of length ${length} that ${why}, so it was dropped`);
|
|
892
|
+
}
|
package/src/mcpl/push-handler.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
11
|
-
import {
|
|
11
|
+
import { convertBlocks } from './convert-block.js';
|
|
12
12
|
|
|
13
13
|
import type {
|
|
14
14
|
PushEventParams,
|
|
@@ -204,7 +204,7 @@ export class PushHandler {
|
|
|
204
204
|
|
|
205
205
|
// 2. Convert content blocks, and refuse content with nothing visible in
|
|
206
206
|
// it — before dedup, so a refused eventId is not burned for a retry.
|
|
207
|
-
const content: ContentBlock[] = params.payload.content
|
|
207
|
+
const content: ContentBlock[] = convertBlocks(params.payload.content, 'attachment on push event', serverId);
|
|
208
208
|
if (!emptyAllowed && isVisiblyEmptyConversion(params.payload.content, content)) {
|
|
209
209
|
const err = new EmptyContentError();
|
|
210
210
|
this.traceEmptyRejection(serverId, params);
|
package/src/mcpl/references.ts
CHANGED
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
* the registry is what makes a future host-mediated fetcher and the
|
|
27
27
|
* code-execution materialization hook possible.
|
|
28
28
|
*/
|
|
29
|
+
import { cutToFit, suppliedValue } from '../supplied-text.js';
|
|
30
|
+
import { NOTICE_TAG } from '../notice-tags.js';
|
|
31
|
+
|
|
29
32
|
|
|
30
33
|
export const REFERENCE_LIMITS = { uri: 4096, mimeType: 255, name: 255, expiresAt: 64 } as const;
|
|
31
34
|
export const DIGEST_PATTERN = /^sha256:[A-Za-z0-9_-]{43}$/;
|
|
@@ -210,7 +213,7 @@ export function isInlineContradiction(block: unknown): boolean {
|
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
export const INLINE_WITHHELD_TEXT =
|
|
213
|
-
|
|
216
|
+
`[${NOTICE_TAG.contentNotShown}: its inline data arrived marked as a reference too, so this host withheld it.]`;
|
|
214
217
|
|
|
215
218
|
/*
|
|
216
219
|
* What the model-visible lanes (push, channel, context injection and tool
|
|
@@ -220,28 +223,28 @@ export const INLINE_WITHHELD_TEXT =
|
|
|
220
223
|
* agent to do about any of them. The text marker is NON_STRING_TEXT_PLACEHOLDER
|
|
221
224
|
* (visible-content.ts).
|
|
222
225
|
*/
|
|
223
|
-
export const IMAGE_NO_MEDIA_TYPE_TEXT =
|
|
224
|
-
export const IMAGE_NO_DATA_TEXT =
|
|
225
|
-
export const AUDIO_NO_MEDIA_TYPE_TEXT =
|
|
226
|
+
export const IMAGE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.imageNotShown}: its data arrived without a media type.]`;
|
|
227
|
+
export const IMAGE_NO_DATA_TEXT = `[${NOTICE_TAG.imageNotShown}: it arrived with no usable data or link.]`;
|
|
228
|
+
export const AUDIO_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.audioNotShown}: its data arrived without a media type.]`;
|
|
226
229
|
/** On the push, channel and context-injection lanes, audio shows only as
|
|
227
230
|
* inline data, so a plain link isn't enough either. (A tool result's audio
|
|
228
231
|
* with a link is a reference, as any block with one is there.) */
|
|
229
|
-
export const AUDIO_NO_DATA_TEXT =
|
|
232
|
+
export const AUDIO_NO_DATA_TEXT = `[${NOTICE_TAG.audioNotShown}: it arrived with no usable inline data.]`;
|
|
230
233
|
/** MCP's embedded resource whose contents are neither text nor a blob, or
|
|
231
234
|
* both. */
|
|
232
|
-
export const EMBEDDED_RESOURCE_UNREADABLE_TEXT =
|
|
235
|
+
export const EMBEDDED_RESOURCE_UNREADABLE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its contents arrived as neither text nor data, or as both.]`;
|
|
233
236
|
/** MCP's embedded resource whose blob has no media type, so it can't be
|
|
234
237
|
* shown as an image or audio. */
|
|
235
|
-
export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT =
|
|
238
|
+
export const EMBEDDED_RESOURCE_NO_MEDIA_TYPE_TEXT = `[${NOTICE_TAG.resourceNotShown}: its data arrived without a media type.]`;
|
|
236
239
|
|
|
237
240
|
/** MCP's embedded resource whose blob is of a type no lane can show: what
|
|
238
241
|
* it was, by its type and size. Made well formed (a lone UTF-16 surrogate
|
|
239
242
|
* becomes U+FFFD), as a stub is (#695), since it names the block's media
|
|
240
|
-
* type,
|
|
241
|
-
* text. */
|
|
243
|
+
* type, whose lone surrogates referenceTypeLabel leaves as they are: every
|
|
244
|
+
* lane then shows the same text. */
|
|
242
245
|
export function embeddedBinaryText(mimeType: string, blob: string): string {
|
|
243
246
|
const what = [referenceTypeLabel(mimeType), formatSizeBytes(Math.floor(blob.length * 3 / 4))].filter(Boolean).join(', ');
|
|
244
|
-
return `[
|
|
247
|
+
return `[${NOTICE_TAG.resourceNotShown}: this host can't display its data (${what}).]`.toWellFormed();
|
|
245
248
|
}
|
|
246
249
|
/**
|
|
247
250
|
* What the converter (convertBlock, on every lane) shows for a text
|
|
@@ -250,10 +253,10 @@ export function embeddedBinaryText(mimeType: string, blob: string): string {
|
|
|
250
253
|
* sender's, so it never makes content visible (isVisiblyEmptyConversion in
|
|
251
254
|
* visible-content.ts, which re-exports it).
|
|
252
255
|
*/
|
|
253
|
-
export const NON_STRING_TEXT_PLACEHOLDER =
|
|
254
|
-
export const REFERENCE_NO_LINK_TEXT =
|
|
255
|
-
export const REFERENCE_LINK_TOO_LONG_TEXT =
|
|
256
|
-
const UNREADABLE_BLOCK_TEXT =
|
|
256
|
+
export const NON_STRING_TEXT_PLACEHOLDER = `[${NOTICE_TAG.textNotShown}: a text part arrived without readable text.]`;
|
|
257
|
+
export const REFERENCE_NO_LINK_TEXT = `[${NOTICE_TAG.referenceNotShown}: it arrived without a link.]`;
|
|
258
|
+
export const REFERENCE_LINK_TOO_LONG_TEXT = `[${NOTICE_TAG.referenceNotShown}: its link is longer than this host accepts.]`;
|
|
259
|
+
const UNREADABLE_BLOCK_TEXT = `[${NOTICE_TAG.contentNotShown}: a part arrived in a form this host can't read.]`;
|
|
257
260
|
|
|
258
261
|
/** A block of a type the host doesn't show, of no type at all, or no block.
|
|
259
262
|
* The type is the sender's word, so it's shown as a label is: bounded, with
|
|
@@ -261,10 +264,10 @@ const UNREADABLE_BLOCK_TEXT = "[Content not shown: a part arrived in a form this
|
|
|
261
264
|
* converted to one. */
|
|
262
265
|
export function unknownBlockText(block: unknown): string {
|
|
263
266
|
if (!isRecord(block)) return UNREADABLE_BLOCK_TEXT;
|
|
264
|
-
const label = typeof block.type === 'string' ?
|
|
267
|
+
const label = typeof block.type === 'string' ? suppliedLabel(block.type, STUB_FIELD_CHARS) : '';
|
|
265
268
|
return label
|
|
266
|
-
? `[
|
|
267
|
-
:
|
|
269
|
+
? `[${NOTICE_TAG.contentNotShown}: this host can't display "${label}" content.]`
|
|
270
|
+
: `[${NOTICE_TAG.contentNotShown}: a part arrived with no type.]`;
|
|
268
271
|
}
|
|
269
272
|
|
|
270
273
|
/**
|
|
@@ -654,6 +657,11 @@ export function sanitizeLabel(s: string, maxChars: number): string {
|
|
|
654
657
|
return cut + '…';
|
|
655
658
|
}
|
|
656
659
|
|
|
660
|
+
function suppliedLabel(s: string, maxChars: number): string {
|
|
661
|
+
const shown = suppliedValue(s).replace(/[\p{Cc}\p{Bidi_Control}]/gu, '').trim();
|
|
662
|
+
return shown.length <= maxChars ? shown : `${cutToFit(shown, maxChars - 1)}…`;
|
|
663
|
+
}
|
|
664
|
+
|
|
657
665
|
export function formatSizeBytes(n: number): string {
|
|
658
666
|
if (n < 1024) return `${n}B`;
|
|
659
667
|
if (n < 1024 * 1024) return `~${Math.round(n / 1024)}KB`;
|
|
@@ -786,7 +794,7 @@ export class ReferenceRegistry {
|
|
|
786
794
|
return existing;
|
|
787
795
|
}
|
|
788
796
|
}
|
|
789
|
-
const refId =
|
|
797
|
+
const refId = `${NOTICE_TAG.referenceId}_${(++this.counter).toString(36)}_${Math.random().toString(36).slice(2, 2 + REF_ID_SUFFIX_CHARS)}`;
|
|
790
798
|
const record: ReferenceRecord = { refId, serverIds: serverId === undefined ? [] : [serverId], testimony, receivedAt: Date.now() };
|
|
791
799
|
this.records.set(refId, record);
|
|
792
800
|
this.byKey.set(testimony.uri, refId);
|
|
@@ -871,7 +879,7 @@ export interface ScriptLine {
|
|
|
871
879
|
/** A reference's type as its stub shows one, or '' when nothing is left of
|
|
872
880
|
* it once sanitized (the stub then leaves it out). */
|
|
873
881
|
export function referenceTypeLabel(mimeType: string | undefined): string {
|
|
874
|
-
return mimeType ?
|
|
882
|
+
return mimeType ? suppliedLabel(mimeType, STUB_FIELD_CHARS) : '';
|
|
875
883
|
}
|
|
876
884
|
|
|
877
885
|
/**
|
|
@@ -886,7 +894,7 @@ export function referenceTypeLabel(mimeType: string | undefined): string {
|
|
|
886
894
|
* stub, so it's no slot whatever its length.
|
|
887
895
|
*/
|
|
888
896
|
export const REFERENCE_STUB_MAX_CHARS =
|
|
889
|
-
`[
|
|
897
|
+
`[${NOTICE_TAG.referenceId}_${MAX_SAFE.toString(36)}_] `.length + REF_ID_SUFFIX_CHARS
|
|
890
898
|
+ STUB_FIELD_CHARS
|
|
891
899
|
+ ' — '.length + STUB_FIELD_CHARS + ', '.length + `${formatSizeBytes(MAX_SAFE)} claimed`.length
|
|
892
900
|
+ ' — '.length + STUB_FIELD_CHARS
|
|
@@ -1099,7 +1107,7 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
|
|
|
1099
1107
|
const nowMs = Date.now();
|
|
1100
1108
|
const parts: string[] = [];
|
|
1101
1109
|
// A label is shown only when something is left of it once sanitized.
|
|
1102
|
-
const name = t.name ?
|
|
1110
|
+
const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
|
|
1103
1111
|
if (name) parts.push(name);
|
|
1104
1112
|
const meta: string[] = [];
|
|
1105
1113
|
const mimeType = referenceTypeLabel(t.mimeType);
|
|
@@ -1130,9 +1138,9 @@ export function buildReferenceStub(t: ReferenceTestimony, provenance?: string, s
|
|
|
1130
1138
|
* server's link shows (StubOptions.unfetchedLinkUri).
|
|
1131
1139
|
*/
|
|
1132
1140
|
function unfetchedLinkText(t: ReferenceTestimony, uri: string): string {
|
|
1133
|
-
const name = t.name ?
|
|
1141
|
+
const name = t.name ? suppliedLabel(t.name, STUB_FIELD_CHARS) : '';
|
|
1134
1142
|
const mimeType = referenceTypeLabel(t.mimeType);
|
|
1135
|
-
return `[
|
|
1143
|
+
return `[${NOTICE_TAG.resourceLink}${name ? ` "${name}"` : ''}: ${uri}${mimeType ? `, ${mimeType}` : ''}; not fetched]`;
|
|
1136
1144
|
}
|
|
1137
1145
|
|
|
1138
1146
|
/**
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
LEGACY_MCP_PROTOCOL_VERSION,
|
|
21
21
|
MODERN_MCP_PROTOCOL_VERSION,
|
|
22
22
|
checkServerConfig,
|
|
23
|
+
logDroppedLine,
|
|
23
24
|
redactServerText,
|
|
24
25
|
redactedData,
|
|
25
26
|
serverTextRedactor,
|
|
@@ -240,19 +241,29 @@ export function toolsetDeclarationsOf(advertised: unknown): Record<string, unkno
|
|
|
240
241
|
/** What the RFC-014 read tap is handed as the connection reads a message
|
|
241
242
|
* that can carry hint changes (see McplServerConnection.setReadTap). */
|
|
242
243
|
export type HintRead =
|
|
243
|
-
/** A response to a request that can carry hints (RFC-014 §6.1 carrier 1)
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
|
|
244
|
+
/** A response to a request that can carry hints (RFC-014 §6.1 carrier 1),
|
|
245
|
+
* or a `tools/list`'s whole inventory (§4.1), its pages joined by
|
|
246
|
+
* sendToolsList, `straddled` when its pages carry different revs.
|
|
247
|
+
* `awaited`: whoever sent it still waits for it; false for a
|
|
248
|
+
* result that arrives after its timeout. A JSON-RPC error carries no state
|
|
249
|
+
* and isn't tapped. */
|
|
250
|
+
| { kind: 'result'; method: string; params: Record<string, unknown>; result: unknown; awaited: boolean; straddled?: boolean }
|
|
248
251
|
/** A push/event (carrier 2), as read, before the plane gate or the grant
|
|
249
252
|
* admit or refuse it. */
|
|
250
253
|
| { kind: 'push'; params: unknown }
|
|
251
254
|
/** A standalone tools/hints (carrier 3). */
|
|
252
255
|
| { kind: 'hints'; params: unknown };
|
|
253
256
|
|
|
254
|
-
/** Requests whose results can carry hint state (RFC-014 §6.1
|
|
255
|
-
const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list'
|
|
257
|
+
/** Requests whose results can carry hint state, remembered for a late answer (RFC-014 §6.1). */
|
|
258
|
+
const HINTED_METHODS: ReadonlySet<string> = new Set(['tools/call', 'toolsets/open', 'toolsets/close', 'toolsets/list']);
|
|
259
|
+
|
|
260
|
+
function hintedParams(method: string, params: Record<string, unknown>): { toolset?: string; instance?: string } {
|
|
261
|
+
if (method !== 'toolsets/open' && method !== 'toolsets/close') return {};
|
|
262
|
+
return {
|
|
263
|
+
...(typeof params.toolset === 'string' ? { toolset: params.toolset } : {}),
|
|
264
|
+
...(typeof params.instance === 'string' ? { instance: params.instance } : {}),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
256
267
|
|
|
257
268
|
/**
|
|
258
269
|
* A handshake that failed, and whose cleanup couldn't confirm that the child
|
|
@@ -330,6 +341,7 @@ interface PendingRequest {
|
|
|
330
341
|
resolve: (result: unknown) => void;
|
|
331
342
|
reject: (error: Error) => void;
|
|
332
343
|
method: string;
|
|
344
|
+
onRead?: (result: unknown) => void;
|
|
333
345
|
/** Per-request timeout timer; cleared on response/close/timeout. */
|
|
334
346
|
timer?: ReturnType<typeof setTimeout>;
|
|
335
347
|
}
|
|
@@ -339,6 +351,9 @@ interface RequestOptions {
|
|
|
339
351
|
timeoutMs?: number;
|
|
340
352
|
/** Surface a late response even when it carries no checkpoint state. */
|
|
341
353
|
surfaceOrphanedResponse?: boolean;
|
|
354
|
+
/** Called with a successful result as its response is read, before
|
|
355
|
+
* anything read after it and before the request's promise settles. */
|
|
356
|
+
onRead?: (result: unknown) => void;
|
|
342
357
|
}
|
|
343
358
|
|
|
344
359
|
/** A server→host message held while its plane is closed. `kept`: it came on
|
|
@@ -452,15 +467,18 @@ export class McplServerConnection extends EventEmitter {
|
|
|
452
467
|
* the message: a push the plane holds or the grant refuses, a result its
|
|
453
468
|
* caller stopped waiting for. The tap sees every such message of the
|
|
454
469
|
* current transport, synchronously, before anything routes it; a late
|
|
455
|
-
* answer's request is remembered past its timeout (hintedRequests
|
|
456
|
-
*
|
|
470
|
+
* answer's request is remembered past its timeout (hintedRequests, while
|
|
471
|
+
* no more than 1,000 hinted requests await an answer) so its hints still
|
|
472
|
+
* apply. Null (the default) taps nothing, and the connection
|
|
457
473
|
* behaves as it always has.
|
|
458
474
|
*/
|
|
459
475
|
private readTap: ((read: HintRead, epoch: number) => void) | null = null;
|
|
460
476
|
/** The hinted requests sent on this transport, by id, kept past a timeout
|
|
461
|
-
* so a late result is still tapped
|
|
477
|
+
* so a late result is still tapped: each one's method, and an open's or
|
|
478
|
+
* close's toolset and instance, which are all the read of its result
|
|
479
|
+
* uses; never a call's arguments. Bounded. Cleared at each boundary,
|
|
462
480
|
* since the next transport numbers its requests afresh. */
|
|
463
|
-
private hintedRequests = new Map<string | number, { method: string; params:
|
|
481
|
+
private hintedRequests = new Map<string | number, { method: string; params: { toolset?: string; instance?: string } }>();
|
|
464
482
|
|
|
465
483
|
/** Install (or, with null, remove) the RFC-014 read tap (see readTap). */
|
|
466
484
|
setReadTap(tap: ((read: HintRead, epoch: number) => void) | null): void {
|
|
@@ -1313,13 +1331,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1313
1331
|
reject(closedWhileConnecting());
|
|
1314
1332
|
};
|
|
1315
1333
|
const onLine = (line: string) => {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
msg = JSON.parse(line) as JsonRpcResponse;
|
|
1319
|
-
} catch {
|
|
1320
|
-
return; // Ignore non-JSON lines (e.g. logback output from Java servers)
|
|
1321
|
-
}
|
|
1322
|
-
if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
|
|
1334
|
+
const msg = messageFromLine(config.id, line, transport) as JsonRpcResponse | undefined;
|
|
1335
|
+
if (!msg) return;
|
|
1323
1336
|
// A request or a notification from the server is never the
|
|
1324
1337
|
// initialize response, whatever its id: a server's own ids can start
|
|
1325
1338
|
// at 0, this handshake's. MCP lets a server ping before
|
|
@@ -1708,22 +1721,40 @@ export class McplServerConnection extends EventEmitter {
|
|
|
1708
1721
|
* read it as a complete page). A page that isn't well formed (no `tools` array, a cursor
|
|
1709
1722
|
* that isn't a string), or cursors that don't terminate (a repeat, or more
|
|
1710
1723
|
* than {@link MAX_TOOL_LIST_PAGES} pages), are errors, never a partial or
|
|
1711
|
-
* empty inventory published as complete.
|
|
1724
|
+
* empty inventory published as complete. The read tap (RFC-014) is handed
|
|
1725
|
+
* the complete inventory once, with the last page's `_meta`, as the last
|
|
1726
|
+
* page's response is read, before anything read after it; a list that
|
|
1727
|
+
* fails or times out hands it nothing. It's `straddled` when its pages
|
|
1728
|
+
* carry different `mcpl/rev`s.
|
|
1712
1729
|
*/
|
|
1713
1730
|
async sendToolsList(): Promise<{ tools: McpToolDefinition[] }> {
|
|
1714
1731
|
const tools: McpToolDefinition[] = [];
|
|
1715
1732
|
const seen = new Set<string>();
|
|
1716
1733
|
let cursor: string | undefined;
|
|
1717
1734
|
let first: Record<string, unknown> | undefined;
|
|
1735
|
+
const read: McpToolDefinition[] = [];
|
|
1736
|
+
const revs = new Set<unknown>();
|
|
1737
|
+
const onRead = (result: unknown) => {
|
|
1738
|
+
if (!result || typeof result !== 'object' || !Array.isArray((result as { tools?: unknown }).tools)) return;
|
|
1739
|
+
const r = result as { tools: McpToolDefinition[]; nextCursor?: unknown; _meta?: unknown };
|
|
1740
|
+
for (const t of r.tools) read.push(t);
|
|
1741
|
+
const rev = (r._meta as Record<string, unknown> | undefined)?.['mcpl/rev'];
|
|
1742
|
+
if (typeof rev === 'number') revs.add(rev);
|
|
1743
|
+
if (r.nextCursor !== undefined && r.nextCursor !== null) return;
|
|
1744
|
+
this.tap({
|
|
1745
|
+
kind: 'result', method: 'tools/list', params: {}, ...(revs.size > 1 ? { straddled: true } : {}),
|
|
1746
|
+
result: { tools: read, ...(r._meta !== undefined ? { _meta: r._meta } : {}) }, awaited: true,
|
|
1747
|
+
});
|
|
1748
|
+
};
|
|
1718
1749
|
for (let page = 0; page < MAX_TOOL_LIST_PAGES; page++) {
|
|
1719
|
-
const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }) as Record<string, unknown> | null;
|
|
1750
|
+
const result = await this.sendRequest('tools/list', cursor === undefined ? {} : { cursor }, { onRead }) as Record<string, unknown> | null;
|
|
1720
1751
|
const malformed = (why: string) =>
|
|
1721
1752
|
new Error(`MCPL server "${this.id}" sent a malformed tools/list page ${page + 1}: ${why}`);
|
|
1722
1753
|
if (!result || typeof result !== 'object' || !Array.isArray(result.tools)) {
|
|
1723
1754
|
throw malformed('no tools array');
|
|
1724
1755
|
}
|
|
1725
1756
|
first ??= result;
|
|
1726
|
-
|
|
1757
|
+
for (const t of result.tools as McpToolDefinition[]) tools.push(t);
|
|
1727
1758
|
if (result.nextCursor === undefined || result.nextCursor === null) {
|
|
1728
1759
|
// The first page's other fields are kept as they came, so a
|
|
1729
1760
|
// single-page answer is returned exactly as before.
|
|
@@ -2176,7 +2207,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2176
2207
|
}
|
|
2177
2208
|
|
|
2178
2209
|
return new Promise((resolve, reject) => {
|
|
2179
|
-
const pending: PendingRequest = { resolve, reject, method };
|
|
2210
|
+
const pending: PendingRequest = { resolve, reject, method, ...(options.onRead ? { onRead: options.onRead } : {}) };
|
|
2180
2211
|
const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
|
|
2181
2212
|
if (timeoutMs > 0) {
|
|
2182
2213
|
pending.timer = setTimeout(() => {
|
|
@@ -2218,7 +2249,7 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2218
2249
|
// RFC-014: a request whose result can carry hints is remembered past
|
|
2219
2250
|
// its timeout, so a late result's hints still apply (readTap).
|
|
2220
2251
|
if (this.readTap && HINTED_METHODS.has(method)) {
|
|
2221
|
-
this.hintedRequests.set(id, { method, params });
|
|
2252
|
+
this.hintedRequests.set(id, { method, params: hintedParams(method, params) });
|
|
2222
2253
|
if (this.hintedRequests.size > 1000) this.hintedRequests.delete(this.hintedRequests.keys().next().value!);
|
|
2223
2254
|
}
|
|
2224
2255
|
try {
|
|
@@ -2341,17 +2372,8 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2341
2372
|
// passed is the dead process's (afterBoundary).
|
|
2342
2373
|
const epoch = this.transportEpoch;
|
|
2343
2374
|
transport.on('line', (line: string) => {
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
msg = JSON.parse(line);
|
|
2347
|
-
} catch {
|
|
2348
|
-
// Ignore non-JSON lines
|
|
2349
|
-
return;
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
// JSON-RPC messages must be objects. Ignore unrelated stdout/log values
|
|
2353
|
-
// before property access, just as malformed JSON lines are ignored.
|
|
2354
|
-
if (msg === null || typeof msg !== 'object' || Array.isArray(msg)) return;
|
|
2375
|
+
const msg = messageFromLine(this.id, line, transport) as JsonRpcRequest | JsonRpcResponse | undefined;
|
|
2376
|
+
if (!msg) return;
|
|
2355
2377
|
try {
|
|
2356
2378
|
this.dispatchMessage(msg, epoch);
|
|
2357
2379
|
} catch (error) {
|
|
@@ -2654,6 +2676,11 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2654
2676
|
logValueRedacted(response.error.message, redact),
|
|
2655
2677
|
);
|
|
2656
2678
|
} else {
|
|
2679
|
+
try {
|
|
2680
|
+
pending.onRead?.(response.result);
|
|
2681
|
+
} catch (error) {
|
|
2682
|
+
console.error(`MCPL server "${this.id}": reading ${pending.method}'s result failed: ${logDetail(describeCause(error))}`);
|
|
2683
|
+
}
|
|
2657
2684
|
pending.resolve(response.result);
|
|
2658
2685
|
}
|
|
2659
2686
|
}
|
|
@@ -2789,3 +2816,18 @@ export class McplServerConnection extends EventEmitter {
|
|
|
2789
2816
|
});
|
|
2790
2817
|
}
|
|
2791
2818
|
}
|
|
2819
|
+
|
|
2820
|
+
function messageFromLine(serverId: string, line: string, transport: McplTransport): object | undefined {
|
|
2821
|
+
let value: unknown;
|
|
2822
|
+
try {
|
|
2823
|
+
value = JSON.parse(line);
|
|
2824
|
+
} catch {
|
|
2825
|
+
logDroppedLine(serverId, line, 'unparsed', transport);
|
|
2826
|
+
return undefined;
|
|
2827
|
+
}
|
|
2828
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
2829
|
+
logDroppedLine(serverId, line, 'not-object', transport);
|
|
2830
|
+
return undefined;
|
|
2831
|
+
}
|
|
2832
|
+
return value;
|
|
2833
|
+
}
|