@animalabs/agent-framework 0.28.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.
Files changed (240) hide show
  1. package/README.md +88 -25
  2. package/dist/src/agent.d.ts.map +1 -1
  3. package/dist/src/agent.js +4 -1
  4. package/dist/src/agent.js.map +1 -1
  5. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  6. package/dist/src/api/mcp-server-core.js +3 -12
  7. package/dist/src/api/mcp-server-core.js.map +1 -1
  8. package/dist/src/api/server.d.ts.map +1 -1
  9. package/dist/src/api/server.js +7 -3
  10. package/dist/src/api/server.js.map +1 -1
  11. package/dist/src/auto-continue.d.ts +2 -0
  12. package/dist/src/auto-continue.d.ts.map +1 -1
  13. package/dist/src/auto-continue.js +5 -2
  14. package/dist/src/auto-continue.js.map +1 -1
  15. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  16. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  17. package/dist/src/code-execution/isolate/runner.js +7 -5
  18. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  19. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.js +3 -2
  21. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  22. package/dist/src/framework.d.ts +164 -36
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +1120 -479
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +34 -10
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +118 -48
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/index.d.ts +1 -1
  31. package/dist/src/gate/index.d.ts.map +1 -1
  32. package/dist/src/gate/index.js +1 -1
  33. package/dist/src/gate/index.js.map +1 -1
  34. package/dist/src/gate/types.d.ts +12 -2
  35. package/dist/src/gate/types.d.ts.map +1 -1
  36. package/dist/src/heartbeat/heartbeat.d.ts +18 -0
  37. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +34 -8
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/held-writes.d.ts +3 -3
  41. package/dist/src/held-writes.js +3 -3
  42. package/dist/src/host-presentation.d.ts +170 -0
  43. package/dist/src/host-presentation.d.ts.map +1 -0
  44. package/dist/src/host-presentation.js +561 -0
  45. package/dist/src/host-presentation.js.map +1 -0
  46. package/dist/src/index.d.ts +2 -2
  47. package/dist/src/index.d.ts.map +1 -1
  48. package/dist/src/index.js +1 -1
  49. package/dist/src/index.js.map +1 -1
  50. package/dist/src/line-reader.d.ts +4 -0
  51. package/dist/src/line-reader.d.ts.map +1 -0
  52. package/dist/src/line-reader.js +59 -0
  53. package/dist/src/line-reader.js.map +1 -0
  54. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  55. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  56. package/dist/src/mcpl/channel-registry.js +131 -53
  57. package/dist/src/mcpl/channel-registry.js.map +1 -1
  58. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  59. package/dist/src/mcpl/conversation-router.js +2 -1
  60. package/dist/src/mcpl/conversation-router.js.map +1 -1
  61. package/dist/src/mcpl/convert-block.d.ts +1 -0
  62. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  63. package/dist/src/mcpl/convert-block.js +16 -4
  64. package/dist/src/mcpl/convert-block.js.map +1 -1
  65. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  66. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  67. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  68. package/dist/src/mcpl/inbound-source.d.ts +3 -1
  69. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  70. package/dist/src/mcpl/inbound-source.js +3 -1
  71. package/dist/src/mcpl/inbound-source.js.map +1 -1
  72. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  73. package/dist/src/mcpl/modern-connection.js +11 -7
  74. package/dist/src/mcpl/modern-connection.js.map +1 -1
  75. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  76. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  77. package/dist/src/mcpl/protocol-family.js +25 -3
  78. package/dist/src/mcpl/protocol-family.js.map +1 -1
  79. package/dist/src/mcpl/push-handler.js +2 -2
  80. package/dist/src/mcpl/push-handler.js.map +1 -1
  81. package/dist/src/mcpl/references.d.ts +12 -40
  82. package/dist/src/mcpl/references.d.ts.map +1 -1
  83. package/dist/src/mcpl/references.js +29 -23
  84. package/dist/src/mcpl/references.js.map +1 -1
  85. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  86. package/dist/src/mcpl/server-connection.js +20 -20
  87. package/dist/src/mcpl/server-connection.js.map +1 -1
  88. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  89. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  90. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  91. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  92. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  93. package/dist/src/mcpl/tool-presence.js +18 -17
  94. package/dist/src/mcpl/tool-presence.js.map +1 -1
  95. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  96. package/dist/src/mcpl/tool-signature.js +17 -16
  97. package/dist/src/mcpl/tool-signature.js.map +1 -1
  98. package/dist/src/mcpl/toolset-host.d.ts +8 -26
  99. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  100. package/dist/src/mcpl/toolset-host.js +16 -6
  101. package/dist/src/mcpl/toolset-host.js.map +1 -1
  102. package/dist/src/mcpl/toolsets-module.d.ts +14 -25
  103. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  104. package/dist/src/mcpl/toolsets-module.js +180 -50
  105. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  106. package/dist/src/mcpl/transport.d.ts +1 -1
  107. package/dist/src/mcpl/transport.d.ts.map +1 -1
  108. package/dist/src/mcpl/transport.js +16 -16
  109. package/dist/src/mcpl/transport.js.map +1 -1
  110. package/dist/src/mcpl/types.d.ts +28 -12
  111. package/dist/src/mcpl/types.d.ts.map +1 -1
  112. package/dist/src/mcpl/types.js.map +1 -1
  113. package/dist/src/module-registry.d.ts +6 -7
  114. package/dist/src/module-registry.d.ts.map +1 -1
  115. package/dist/src/module-registry.js +6 -7
  116. package/dist/src/module-registry.js.map +1 -1
  117. package/dist/src/modules/api/index.d.ts.map +1 -1
  118. package/dist/src/modules/api/index.js +4 -2
  119. package/dist/src/modules/api/index.js.map +1 -1
  120. package/dist/src/modules/discord/index.d.ts.map +1 -1
  121. package/dist/src/modules/discord/index.js +33 -25
  122. package/dist/src/modules/discord/index.js.map +1 -1
  123. package/dist/src/modules/history/index.js +8 -6
  124. package/dist/src/modules/history/index.js.map +1 -1
  125. package/dist/src/modules/sender-name.d.ts +7 -0
  126. package/dist/src/modules/sender-name.d.ts.map +1 -0
  127. package/dist/src/modules/sender-name.js +10 -0
  128. package/dist/src/modules/sender-name.js.map +1 -0
  129. package/dist/src/modules/workspace/answers.d.ts +5 -1
  130. package/dist/src/modules/workspace/answers.d.ts.map +1 -1
  131. package/dist/src/modules/workspace/answers.js +16 -1
  132. package/dist/src/modules/workspace/answers.js.map +1 -1
  133. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.js +6 -4
  135. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  136. package/dist/src/modules/workspace/effects.d.ts +4 -2
  137. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  138. package/dist/src/modules/workspace/effects.js +50 -40
  139. package/dist/src/modules/workspace/effects.js.map +1 -1
  140. package/dist/src/modules/workspace/index.d.ts +1 -0
  141. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  142. package/dist/src/modules/workspace/index.js +66 -36
  143. package/dist/src/modules/workspace/index.js.map +1 -1
  144. package/dist/src/modules/workspace/observe.d.ts +51 -20
  145. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  146. package/dist/src/modules/workspace/observe.js +120 -82
  147. package/dist/src/modules/workspace/observe.js.map +1 -1
  148. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  149. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  150. package/dist/src/modules/workspace/reconcile.js +133 -37
  151. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  152. package/dist/src/notice-tags.d.ts +79 -0
  153. package/dist/src/notice-tags.d.ts.map +1 -0
  154. package/dist/src/notice-tags.js +83 -0
  155. package/dist/src/notice-tags.js.map +1 -0
  156. package/dist/src/reasoning-loss-notice.d.ts.map +1 -1
  157. package/dist/src/reasoning-loss-notice.js +5 -3
  158. package/dist/src/reasoning-loss-notice.js.map +1 -1
  159. package/dist/src/silent-heartbeat.d.ts +1 -53
  160. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  161. package/dist/src/silent-heartbeat.js +56 -3
  162. package/dist/src/silent-heartbeat.js.map +1 -1
  163. package/dist/src/supplied-text.d.ts +35 -0
  164. package/dist/src/supplied-text.d.ts.map +1 -0
  165. package/dist/src/supplied-text.js +141 -0
  166. package/dist/src/supplied-text.js.map +1 -0
  167. package/dist/src/tool-args.d.ts.map +1 -1
  168. package/dist/src/tool-args.js +9 -8
  169. package/dist/src/tool-args.js.map +1 -1
  170. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  171. package/dist/src/tool-image-ledger.js +2 -1
  172. package/dist/src/tool-image-ledger.js.map +1 -1
  173. package/dist/src/tool-presentation.d.ts.map +1 -1
  174. package/dist/src/tool-presentation.js +5 -4
  175. package/dist/src/tool-presentation.js.map +1 -1
  176. package/dist/src/tune-out/coordinator.d.ts +3 -2
  177. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  178. package/dist/src/tune-out/coordinator.js +24 -26
  179. package/dist/src/tune-out/coordinator.js.map +1 -1
  180. package/dist/src/types/agent.d.ts +23 -9
  181. package/dist/src/types/agent.d.ts.map +1 -1
  182. package/dist/src/types/framework.d.ts +43 -0
  183. package/dist/src/types/framework.d.ts.map +1 -1
  184. package/dist/src/types/module.d.ts +22 -11
  185. package/dist/src/types/module.d.ts.map +1 -1
  186. package/package.json +2 -2
  187. package/src/agent.ts +4 -1
  188. package/src/api/mcp-server-core.ts +3 -14
  189. package/src/api/server.ts +7 -3
  190. package/src/auto-continue.ts +8 -2
  191. package/src/code-execution/isolate/runner.ts +7 -5
  192. package/src/code-execution/isolate/scripts.ts +3 -2
  193. package/src/framework.ts +1132 -492
  194. package/src/gate/event-gate.ts +128 -47
  195. package/src/gate/index.ts +1 -1
  196. package/src/gate/types.ts +12 -2
  197. package/src/heartbeat/heartbeat.ts +48 -9
  198. package/src/held-writes.ts +3 -3
  199. package/src/host-presentation.ts +605 -0
  200. package/src/index.ts +2 -2
  201. package/src/line-reader.ts +58 -0
  202. package/src/mcpl/channel-registry.ts +137 -52
  203. package/src/mcpl/conversation-router.ts +2 -1
  204. package/src/mcpl/convert-block.ts +17 -4
  205. package/src/mcpl/hook-orchestrator.ts +4 -3
  206. package/src/mcpl/inbound-source.ts +3 -1
  207. package/src/mcpl/modern-connection.ts +11 -7
  208. package/src/mcpl/protocol-family.ts +24 -3
  209. package/src/mcpl/push-handler.ts +2 -2
  210. package/src/mcpl/references.ts +31 -23
  211. package/src/mcpl/server-connection.ts +20 -18
  212. package/src/mcpl/skip-reply-wake.ts +35 -0
  213. package/src/mcpl/tool-presence.ts +18 -17
  214. package/src/mcpl/tool-signature.ts +17 -16
  215. package/src/mcpl/toolset-host.ts +18 -6
  216. package/src/mcpl/toolsets-module.ts +142 -50
  217. package/src/mcpl/transport.ts +18 -14
  218. package/src/mcpl/types.ts +29 -12
  219. package/src/module-registry.ts +6 -7
  220. package/src/modules/api/index.ts +4 -2
  221. package/src/modules/discord/index.ts +37 -25
  222. package/src/modules/history/index.ts +8 -6
  223. package/src/modules/sender-name.ts +9 -0
  224. package/src/modules/workspace/answers.ts +20 -2
  225. package/src/modules/workspace/direct-write.ts +4 -3
  226. package/src/modules/workspace/effects.ts +50 -40
  227. package/src/modules/workspace/index.ts +58 -34
  228. package/src/modules/workspace/observe.ts +139 -88
  229. package/src/modules/workspace/reconcile.ts +159 -36
  230. package/src/notice-tags.ts +84 -0
  231. package/src/reasoning-loss-notice.ts +5 -3
  232. package/src/silent-heartbeat.ts +4 -3
  233. package/src/supplied-text.ts +154 -0
  234. package/src/tool-args.ts +9 -8
  235. package/src/tool-image-ledger.ts +2 -1
  236. package/src/tool-presentation.ts +5 -4
  237. package/src/tune-out/coordinator.ts +25 -27
  238. package/src/types/agent.ts +23 -9
  239. package/src/types/framework.ts +44 -0
  240. package/src/types/module.ts +23 -11
@@ -24,6 +24,7 @@
24
24
  * which the toolsets module gathers at its next start as a stored
25
25
  * `afterUser` injection.
26
26
  */
27
+ import { nameToTypeBack, quotedValue } from '../supplied-text.js';
27
28
  import { McplRequestError, type McplServerConnection, type HintRead } from './server-connection.js';
28
29
  import type { McplServerConfig } from './types.js';
29
30
  import { isToolAllowed } from './tool-policy.js';
@@ -73,6 +74,10 @@ export interface ToolsetHostDeps {
73
74
  * where it isn't published. RFC §4.2: until its move applies, a tool
74
75
  * keeps working where it is. */
75
76
  published?(serverId: string, tool: string): Presence | undefined;
77
+ /** The host's own toolsets an agent has open (the built-in one, server
78
+ * tabs): each one's marker and reminder, for RFC §8.3 reminders beside the
79
+ * servers' toolsets. */
80
+ hostOpen?(agentName: string): Array<{ marker: string; reminder: () => string; dueWhenAbsent?: boolean }>;
76
81
  }
77
82
 
78
83
  /** One server's RFC-014 state and traffic. Lives as long as the server's
@@ -381,8 +386,8 @@ export class ToolsetController {
381
386
  */
382
387
  dynamicCallRefusal(tool: string, calledAs: string): string | null {
383
388
  if (!this.presence.active || !this.presence.tool(tool) || this.presence.shownAs(tool, 'static')) return null;
384
- return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${tool} is a dynamic tool of ${this.serverId}, ` +
385
- `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${tool}", args}. ${this.presence.toolStatus(tool)}.`;
389
+ return `There's no tool "${calledAs}" in your tool list, so nothing ran: ${nameToTypeBack(tool)} is a dynamic tool of ${this.serverId}, ` +
390
+ `called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(tool)}, args}. ${this.presence.toolStatus(tool)}.`;
386
391
  }
387
392
 
388
393
  /**
@@ -621,8 +626,8 @@ export class ToolsetController {
621
626
  if (!moved.length) return;
622
627
  const cs = emptyChange();
623
628
  cs.notes.push(...moved.map(({ name, to }) => to === 'dynamic'
624
- ? `${name} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:"${name}", args}.`
625
- : `${name} is in your tool list now, as ${this.deps.listedName(this.serverId, name)}.`));
629
+ ? `${nameToTypeBack(name)} left your tool list: it's announced when available, and called with ${META_TOOL_NAMES.act} {server:"${this.serverId}", tool:${quotedValue(name)}, args}.`
630
+ : `${nameToTypeBack(name)} is in your tool list now, as ${nameToTypeBack(this.deps.listedName(this.serverId, name))}.`));
626
631
  this.deliver(cs);
627
632
  }
628
633
  }
@@ -681,8 +686,9 @@ export class ToolsetHub {
681
686
  * reminder due.
682
687
  */
683
688
  observeCompile(agentName: string, messages: readonly unknown[]): void {
689
+ const host = this.deps.hostOpen?.(agentName) ?? [];
684
690
  const open = this.openMarkers();
685
- if (open.length === 0) {
691
+ if (open.length === 0 && host.length === 0) {
686
692
  this.seen.delete(agentName);
687
693
  this.due.delete(agentName);
688
694
  return;
@@ -696,6 +702,9 @@ export class ToolsetHub {
696
702
  }
697
703
  for (const marker of [...seen]) if (!open.includes(marker)) seen.delete(marker);
698
704
  this.seen.set(agentName, seen);
705
+ // A host toolset open across a restart was never seen here: it's due
706
+ // whenever a request doesn't carry its marker.
707
+ for (const h of host) if (h.dueWhenAbsent && !text.includes(h.marker)) missing = true;
699
708
  // Due until a request carries every marker the agent has seen again: a
700
709
  // gather that doesn't send (a preview) leaves it due for the next start.
701
710
  if (missing) this.due.add(agentName);
@@ -705,7 +714,10 @@ export class ToolsetHub {
705
714
  /** The reminder due for `agentName`, or null when none is. */
706
715
  reminderFor(agentName: string): string | null {
707
716
  if (!this.due.has(agentName)) return null;
708
- const parts = this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r);
717
+ const parts = [
718
+ ...(this.deps.hostOpen?.(agentName) ?? []).map((h) => h.reminder()),
719
+ ...this.presenting().filter((c) => this.deps.canNotify(c.serverId)).map((c) => c.presence.reminder()).filter((r): r is string => !!r),
720
+ ];
709
721
  if (parts.length === 0) return null;
710
722
  return `Open toolsets (a reminder: their earlier announcements have left your context):\n${parts.join('\n')}`;
711
723
  }
@@ -13,8 +13,9 @@
13
13
  * results, in the results of calls that changed them, in push events and in
14
14
  * notices) and called through `toolsets--act`, which this module only
15
15
  * resolves (resolveAct): the framework dispatches the call it resolves to
16
- * down the same path as a direct call of the server's tool. The module is registered when a server is
17
- * configured with `toolHints: true`; its tools never change after that.
16
+ * down the same path as a direct call of the server's tool. The module is registered at create(),
17
+ * for every agent (the host's own toolsets use it too), unless a module given to create() holds
18
+ * the name; its tools never change after that.
18
19
  *
19
20
  * Its gatherContext publishes the RFC §8.3 reminder, as a stored `afterUser`
20
21
  * injection, when an agent's earlier announcements of an open toolset have
@@ -22,6 +23,7 @@
22
23
  *
23
24
  * Ported from mcpl-cc-bridge's meta-tools.ts, the reference host.
24
25
  */
26
+ import { nameToTypeBack, quotedValue, suppliedValue } from '../supplied-text.js';
25
27
  import type { EventResponse, Module, ModuleContext, ModuleContextInjection, ProcessState } from '../types/module.js';
26
28
  import type { ProcessEvent, ToolCall, ToolDefinition, ToolResult } from '../types/events.js';
27
29
  import { unknownToolMessage } from '../modules/unknown-tool.js';
@@ -29,6 +31,7 @@ import { META_TOOL_NAMES, TOOLSETS_MODULE, type ToolsetController, type ToolsetH
29
31
  import { keyOf, type ChangeSet, type ToolPresence } from './tool-presence.js';
30
32
  import { checkArgs, jsonText, renderToolDefinition, type HintedTool } from './tool-signature.js';
31
33
  import { McplRequestError } from './server-connection.js';
34
+ import { BUILTIN_TOOLSET, type HostPresentation } from '../host-presentation.js';
32
35
 
33
36
  /** The namespace a reminder's stored injection carries. */
34
37
  export const TOOLSETS_REMINDER_NAMESPACE = 'mcpl-toolsets';
@@ -37,24 +40,26 @@ const META_TOOLS: ToolDefinition[] = [
37
40
  {
38
41
  name: 'open',
39
42
  description:
40
- 'Open a toolset of an MCPL server (a game, a building mode, a card table) so its tools can be called with toolsets--act. ' +
41
- 'Returns what the server shows on opening and the tools available now. server may be left out when one server has the toolset, ' +
42
- 'and toolset when the server has only one; instance names a separate copy where a toolset allows several (two games at once); ' +
43
+ 'Open a toolset so its tools can be called with toolsets--act: the host\'s own settings and controls (toolset "settings"), ' +
44
+ 'an MCPL server\'s tab (server alone: its tools without putting them in your tool list; a closed tab is connected again; ' +
45
+ 'full: true puts the server back in your tool list), or a toolset an MCPL server declares (a game, a building mode, a card table: ' +
46
+ 'server and toolset). Returns the tools available now. instance names a separate copy where a toolset allows several; ' +
43
47
  'options as toolsets--look lists them.',
44
48
  inputSchema: {
45
49
  type: 'object',
46
50
  properties: {
47
51
  server: { type: 'string', description: 'the MCPL server id' },
48
- toolset: { type: 'string' },
52
+ toolset: { type: 'string', description: '"settings" for the host\'s own; a server\'s toolset with server' },
49
53
  instance: { type: 'string', description: 'a name for this copy (toolsets with instances only)' },
50
54
  options: { type: 'object', description: 'toolset options, as toolsets--look shows them' },
55
+ full: { type: 'boolean', description: 'with server alone: put the server\'s tools back in your tool list' },
51
56
  },
52
57
  },
53
58
  },
54
59
  {
55
60
  name: 'act',
56
61
  description:
57
- 'Call a tool that was announced in the conversation (a tool of an open toolset, or another dynamic tool); they are not in your tool list. ' +
62
+ 'Call a tool of an open toolset (the host\'s "settings", a server\'s tab, or a server\'s own toolset) or another dynamic tool, as announced in the conversation; they are not in your tool list. ' +
58
63
  'args are the arguments its signature shows; instance picks the open copy when several are open; server is needed only when two servers have the tool. ' +
59
64
  'Returns the server\'s result, then any change it made in which tools are available, unless that change already went out as a notice. toolsets--look shows what is available now.',
60
65
  inputSchema: {
@@ -71,7 +76,7 @@ const META_TOOLS: ToolDefinition[] = [
71
76
  {
72
77
  name: 'look',
73
78
  description:
74
- 'Show what is open and what can be opened on MCPL servers, with the tools usable now: announcements earlier in the conversation may be out of date. ' +
79
+ 'Show what is open and what can be opened (the host\'s "settings", each MCPL server\'s tab, servers\' own toolsets), with the tools usable now: announcements earlier in the conversation may be out of date. ' +
75
80
  'With tool: that tool\'s full description and input schema. With toolset or instance: just that one.',
76
81
  inputSchema: {
77
82
  type: 'object',
@@ -85,13 +90,15 @@ const META_TOOLS: ToolDefinition[] = [
85
90
  },
86
91
  {
87
92
  name: 'close',
88
- description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it.',
93
+ description: 'Close an open toolset or instance: its tools stop, and for a toolset the server declares, the server is told and stops its events too. Closing a game in progress may count as leaving it. ' +
94
+ 'server alone moves a server out of your tool list into a tab (connected, its events still come), or hides its open tab; with disconnect: true it is disconnected until you open it again.',
89
95
  inputSchema: {
90
96
  type: 'object',
91
97
  properties: {
92
98
  server: { type: 'string', description: 'the MCPL server id (may be left out when one server has the toolset open)' },
93
- toolset: { type: 'string', description: 'may be left out when only one thing is open on the server' },
99
+ toolset: { type: 'string', description: '"settings" for the host\'s own; may be left out when only one thing is open on the server' },
94
100
  instance: { type: 'string' },
101
+ disconnect: { type: 'boolean', description: 'with server alone: disconnect it as a closed tab until it is opened again' },
95
102
  },
96
103
  },
97
104
  },
@@ -99,6 +106,21 @@ const META_TOOLS: ToolDefinition[] = [
99
106
 
100
107
  const str = (v: unknown): string | undefined => (typeof v === 'string' && v.trim() ? v.trim() : undefined);
101
108
 
109
+ const selector = (v: unknown, has: (name: string) => boolean): string | undefined =>
110
+ typeof v === 'string' && v !== v.trim() && v.trim() !== '' && has(v) ? v : str(v);
111
+ const presentersOf = (hub: ToolsetHub, server: unknown): ToolsetController[] => {
112
+ const id = str(server);
113
+ if (!id) return hub.presenting();
114
+ const c = hub.get(id);
115
+ return c ? [c] : [];
116
+ };
117
+ const toolNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
118
+ selector(v, (name) => presentersOf(hub, server).some((c) => !!c.presence.tool(name)));
119
+ const toolsetNamed = (hub: ToolsetHub, server: unknown, v: unknown): string | undefined =>
120
+ selector(v, (name) => presentersOf(hub, server).some((c) => c.presence.isDeclared(name) || c.presence.undeclaredGroups.includes(name)));
121
+ const instanceNamed = (presenters: readonly ToolsetController[], toolset: string | undefined, v: unknown): string | undefined =>
122
+ selector(v, (name) => presenters.some((c) => c.presence.openRecords(toolset).some((o) => o.instance === name)));
123
+
102
124
  const SELECTORS = ['server', 'toolset', 'instance', 'tool'] as const;
103
125
 
104
126
  function checkSelectors(args: Record<string, unknown>): void {
@@ -117,7 +139,7 @@ export function withLookHint(serverId: string, text: string): string {
117
139
  }
118
140
 
119
141
  function requestFailure(serverId: string, did: string, unconfirmed: string, error: unknown): never {
120
- const message = error instanceof Error ? error.message : String(error);
142
+ const message = suppliedValue(error instanceof Error ? error.message : String(error));
121
143
  if (error instanceof McplRequestError && error.outcome === 'not-sent') return fail(`${serverId} ${did}: ${message}`);
122
144
  return fail(withLookHint(serverId, `${serverId} ${unconfirmed}: ${message}`));
123
145
  }
@@ -194,7 +216,7 @@ const NONE_PRESENTS = 'no MCPL server presents toolsets now: MCPL tools are call
194
216
 
195
217
  function noToolset(c: ToolsetController, toolset: string): string {
196
218
  const names = [...Object.keys(c.presence.declared), ...c.presence.undeclaredGroups];
197
- return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.join(', ')}` : 'it has none'}`;
219
+ return `${c.serverId} has no toolset "${toolset}"; ${names.length ? `it has ${names.map(nameToTypeBack).join(', ')}` : 'it has none'}`;
198
220
  }
199
221
 
200
222
  /** What a `toolsets--act` call resolves to: the server's tool, its instance and arguments. */
@@ -221,35 +243,35 @@ export function resolveAct(hub: ToolsetHub, input: unknown): ActTarget {
221
243
  try {
222
244
  const args = isObj(input) ? input : {};
223
245
  checkSelectors(args);
224
- const tool = str(args.tool) ?? fail('name the tool');
246
+ const tool = toolNamed(hub, args.server, args.tool) ?? fail('name the tool');
225
247
  const c = owner(hub, args.server, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
226
248
  const p = c.presence;
227
249
  if (!p.tool(tool)) fail(`${c.serverId} has no tool "${tool}": ${META_TOOL_NAMES.look} {server:"${c.serverId}"} shows what's available`);
228
- if (!p.shownAs(tool, 'dynamic')) fail(`${tool} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
250
+ if (!p.shownAs(tool, 'dynamic')) fail(`${nameToTypeBack(tool)} is not a dynamic tool: ${p.toolStatus(tool)}. Call it there.`);
229
251
  if (args.args !== undefined && !isObj(args.args)) fail('args must be an object of the tool\'s arguments');
230
252
  const callArgs = (args.args ?? {}) as Record<string, unknown>;
231
- let instance = str(args.instance);
232
253
  const ts = p.toolsetOf(tool);
254
+ let instance = instanceNamed([c], ts, args.instance);
233
255
  const declared = ts !== undefined && p.isDeclared(ts);
234
256
  const withInstances = hasInstances(p, ts);
235
257
  if (withInstances && !instance) {
236
258
  // RFC §5.5: the instance rides in _meta; with exactly one open, it's that one.
237
259
  const open = p.openRecords(ts);
238
260
  if (open.length === 1) instance = open[0]!.instance;
239
- else if (!open.length) fail(`${tool} belongs to ${ts}, which isn't open: ${p.openHint(ts!)}`);
240
- else fail(`several ${ts} instances are open (${open.map((o) => o.instance).join(', ')}): say which with instance`);
261
+ else if (!open.length) fail(`${nameToTypeBack(tool)} belongs to ${nameToTypeBack(ts!)}, which isn't open: ${p.openHint(ts!)}`);
262
+ else fail(`several ${nameToTypeBack(ts!)} instances are open (${open.map((o) => nameToTypeBack(o.instance!)).join(', ')}): say which with instance`);
241
263
  }
242
- if (instance && !withInstances) fail(`${tool} ${ts ? `belongs to ${ts}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
264
+ if (instance && !withInstances) fail(`${nameToTypeBack(tool)} ${ts ? `belongs to ${nameToTypeBack(ts)}, which has no instances` : 'belongs to no toolset'}: call it without instance`);
243
265
  // RFC §5.4: the host gates calls into an undeclared group it has closed.
244
- if (ts && !declared && !p.isOpen(ts)) fail(`${tool} is in ${ts}, which isn't open: ${p.openHint(ts)}`);
266
+ if (ts && !declared && !p.isOpen(ts)) fail(`${nameToTypeBack(tool)} is in ${nameToTypeBack(ts)}, which isn't open: ${p.openHint(ts)}`);
245
267
  // RFC §6.1: arguments are checked against the definition in force for
246
268
  // that instance: its own from hints (the legal moves of one game), else
247
269
  // the toolset's, else the listed one.
248
270
  const key = actKey(p, tool, instance);
249
271
  const problem = checkArgs(p.defOf(key, tool).inputSchema, callArgs);
250
272
  if (problem) {
251
- fail(`Invalid arguments for ${tool} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
252
- `${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:"${tool}"${instance ? `, instance:"${instance}"` : ''}} shows its current definition.`);
273
+ fail(`Invalid arguments for ${nameToTypeBack(tool)} in ${p.label(key)}: ${problem}. Nothing was sent; ` +
274
+ `${META_TOOL_NAMES.look} {server:"${c.serverId}", tool:${quotedValue(tool)}${instance ? `, instance:${quotedValue(instance)}` : ''}} shows its current definition.`);
253
275
  }
254
276
  return { ok: true, serverId: c.serverId, tool, ...(instance ? { instance } : {}), args: callArgs };
255
277
  } catch (error) {
@@ -267,6 +289,8 @@ export class ToolsetsModule implements Module {
267
289
  constructor(
268
290
  private readonly hub: ToolsetHub,
269
291
  private readonly takerFor: (call: ToolCall) => ((taken: TakenChange) => void) | null,
292
+ /** The host's own toolsets (the built-in "settings" and server tabs), when the framework presents them. */
293
+ private readonly host: HostPresentation | null = null,
270
294
  ) {}
271
295
 
272
296
  async start(_ctx: ModuleContext): Promise<void> {}
@@ -291,7 +315,16 @@ export class ToolsetsModule implements Module {
291
315
  const args = isObj(call.input) ? call.input : {};
292
316
  try {
293
317
  checkSelectors(args);
318
+ const hosted = await this.hostCall(call.name, args, call.callerAgentName);
319
+ if (hosted) return hosted;
294
320
  const take = this.takerFor(call);
321
+ const lead = call.name === 'look' ? this.hostLookLead(args, call.callerAgentName) : null;
322
+ if (lead !== null) {
323
+ if (!this.hub.presenting().length) return textResult(lead);
324
+ const theirs = await this.look(args, take);
325
+ if (theirs.isError || typeof theirs.data !== 'string') return theirs;
326
+ return textResult(`${lead}\n\n${theirs.data}`);
327
+ }
295
328
  switch (call.name) {
296
329
  case 'open': return await this.open(args, take);
297
330
  case 'act': return this.act(args);
@@ -307,6 +340,62 @@ export class ToolsetsModule implements Module {
307
340
  }
308
341
  }
309
342
 
343
+ /**
344
+ * The host's own toolsets: `toolset: "settings"` alone, or `server` alone
345
+ * (a server's tab), for open, close and look; a tool a host toolset has, for
346
+ * look. Null for everything else, which the servers' toolsets answer.
347
+ */
348
+ private async hostCall(name: string, args: Record<string, unknown>, agentName: string | undefined): Promise<ToolResult | null> {
349
+ const host = this.host;
350
+ if (!host || !agentName || !host.active(agentName)) return null;
351
+ const server = str(args.server);
352
+ const toolset = str(args.toolset);
353
+ const instance = str(args.instance);
354
+ const tool = str(args.tool);
355
+ const builtin = toolset === BUILTIN_TOOLSET && !server && !instance;
356
+ const tab = !!server && !toolset && !instance && args.options === undefined;
357
+ if (args.full !== undefined && typeof args.full !== 'boolean') fail('full must be true or false');
358
+ if (args.disconnect !== undefined && typeof args.disconnect !== 'boolean') fail('disconnect must be true or false');
359
+ if (tab && !host.knows(server!) && !this.hub.get(server!)) fail(`no MCPL server "${server}" is configured here`);
360
+ switch (name) {
361
+ case 'open':
362
+ if (builtin) return textResult(host.openBuiltin(agentName));
363
+ if (tab) return textResult(await host.openServer(agentName, server!, { full: args.full === true }));
364
+ if (args.full !== undefined) fail('full goes with server alone');
365
+ return null;
366
+ case 'close':
367
+ if (builtin) return textResult(host.closeBuiltin(agentName));
368
+ if (tab) return textResult(await host.closeServer(agentName, server!, { disconnect: args.disconnect === true }));
369
+ if (args.disconnect !== undefined) fail('disconnect goes with server alone');
370
+ return null;
371
+ case 'look': {
372
+ // A host toolset's tool, with or without its server named (as act takes it).
373
+ if (tool && !toolset && !instance) {
374
+ const shown = host.lookTool(agentName, tool, server);
375
+ if (shown) return textResult(shown);
376
+ return null;
377
+ }
378
+ if (builtin) return textResult(host.lookToolset(agentName, { builtin: true }));
379
+ // A server's tab, and every host toolset with no selector, go first in
380
+ // what the servers' own look says (handleToolCall adds them).
381
+ if (tab && !tool && !this.hub.get(server!)?.presents) return textResult(host.lookToolset(agentName, { serverId: server! }));
382
+ return null;
383
+ }
384
+ default:
385
+ return null;
386
+ }
387
+ }
388
+
389
+ /** What the host's own toolsets add ahead of the servers' look: everything, or one presenting server's tab. */
390
+ private hostLookLead(args: Record<string, unknown>, agentName: string | undefined): string | null {
391
+ const host = this.host;
392
+ if (!host || !agentName || !host.active(agentName)) return null;
393
+ const server = str(args.server);
394
+ if (str(args.toolset) || str(args.instance) || str(args.tool)) return null;
395
+ if (!server) return host.catalog(agentName).join('\n');
396
+ return host.knows(server) ? host.lookToolset(agentName, { serverId: server }) : null;
397
+ }
398
+
310
399
  private toolsAt(c: ToolsetController, key: string): HintedTool[] {
311
400
  return [...(c.presence.view().get(key)?.tools.values() ?? [])];
312
401
  }
@@ -320,36 +409,36 @@ export class ToolsetsModule implements Module {
320
409
 
321
410
  private async open(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
322
411
  const shown = take !== null;
323
- let toolset = str(args.toolset);
412
+ let toolset = toolsetNamed(this.hub, args.server, args.toolset);
324
413
  const c = toolset && !str(args.server)
325
414
  ? owner(this.hub, undefined, (x) => x.presence.isDeclared(toolset!) || x.presence.undeclaredGroups.includes(toolset!), `a toolset "${toolset}"`)
326
415
  : pick(this.hub, args.server);
327
416
  const p = c.presence;
328
417
  const openable = [...Object.keys(p.declared), ...p.undeclaredGroups];
329
418
  if (!toolset) {
330
- if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.join(', ')}` : 'no toolsets'}`);
419
+ if (openable.length !== 1) fail(`${c.serverId} has ${openable.length ? `several toolsets: name one of ${openable.map(nameToTypeBack).join(', ')}` : 'no toolsets'}`);
331
420
  toolset = openable[0]!;
332
421
  }
333
- const instance = str(args.instance);
422
+ const instance = instanceNamed([c], toolset, args.instance);
334
423
  if (args.options !== undefined && !isObj(args.options)) fail('options must be an object');
335
424
  const options = args.options as Record<string, unknown> | undefined;
336
425
 
337
426
  if (p.isDeclared(toolset)) {
338
- if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${toolset} has no instances: open it without one`);
427
+ if (instance && !p.declared[toolset]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset)} has no instances: open it without one`);
339
428
  let opened: Awaited<ReturnType<ToolsetController['open']>>;
340
429
  try {
341
430
  opened = await c.open(toolset, instance, options, { shown });
342
431
  } catch (error) {
343
- return requestFailure(c.serverId, `did not open ${toolset}`, `didn't confirm opening ${toolset}`, error);
432
+ return requestFailure(c.serverId, `did not open ${nameToTypeBack(toolset)}`, `didn't confirm opening ${nameToTypeBack(toolset)}`, error);
344
433
  }
345
434
  // RFC §5.2: opening what is open adopts it; its content says which options weren't applied.
346
435
  const blocks: Block[] = [
347
- { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${opened.title}` : ''}${opened.adopted ? ' (already open)' : ''}` },
436
+ { type: 'text', text: `${p.label(opened.key)}${opened.title ? ` ${suppliedValue(opened.title)}` : ''}${opened.adopted ? ' (already open)' : ''}` },
348
437
  ...asBlocks(opened.content),
349
438
  ];
350
439
  const tail = [...p.toolBlock(opened.key, this.toolsAt(c, opened.key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
351
440
  const gone = opened.change.changes.find((ch) => ch.key === opened.key)?.gone ?? [];
352
- if (gone.length) tail.push(`now unavailable: ${gone.join(', ')}`);
441
+ if (gone.length) tail.push(`now unavailable: ${gone.map(suppliedValue).join(', ')}`);
353
442
  const others = this.renderOthers(c, opened.change, opened.key, shown);
354
443
  if (others) tail.push(others);
355
444
  const close = p.closeHint(opened.key);
@@ -360,7 +449,7 @@ export class ToolsetsModule implements Module {
360
449
  }
361
450
  if (p.undeclaredGroups.includes(toolset)) {
362
451
  // RFC §5.4: a grouping the server offers but doesn't track: the host opens it, the server isn't told.
363
- if (instance || options) fail(`${c.serverId}/${toolset} takes no instance or options (the server doesn't declare it)`);
452
+ if (instance || options) fail(`${c.serverId}/${nameToTypeBack(toolset)} takes no instance or options (the server doesn't declare it)`);
364
453
  const cs = c.setGroupOpen(toolset, true);
365
454
  const key = keyOf(toolset);
366
455
  const lines = [`${p.label(key)} open.`, ...p.toolBlock(key, this.toolsAt(c, key), { headed: false, describeAll: !shown, seen: shown ? undefined : new Set<string>() })];
@@ -400,7 +489,7 @@ export class ToolsetsModule implements Module {
400
489
  };
401
490
  let result: ToolResult;
402
491
  try {
403
- result = this.lookNow(args, server, str(args.tool), shown);
492
+ result = this.lookNow(args, server, toolNamed(this.hub, server, args.tool), shown);
404
493
  } catch (error) {
405
494
  giveBack();
406
495
  if (error instanceof MetaError && unread.length) fail([error.message, ...unread].join('\n'));
@@ -426,13 +515,16 @@ export class ToolsetsModule implements Module {
426
515
 
427
516
  private lookNow(args: Record<string, unknown>, server: string | undefined, tool: string | undefined, mark: boolean): ToolResult {
428
517
  if (!server) {
429
- if (tool) return this.lookTool(owner(this.hub, undefined, (c) => !!c.presence.tool(tool), `a tool "${tool}"`), tool, str(args.instance));
430
- const ts = str(args.toolset);
431
- const inst = str(args.instance);
518
+ if (tool) {
519
+ const c = owner(this.hub, undefined, (x) => !!x.presence.tool(tool), `a tool "${tool}"`);
520
+ return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
521
+ }
522
+ const ts = toolsetNamed(this.hub, undefined, args.toolset);
523
+ const inst = instanceNamed(this.hub.presenting(), ts, args.instance);
432
524
  if (ts || inst) {
433
525
  const c = owner(this.hub,
434
526
  undefined,
435
- (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords().some((o) => o.instance === inst)),
527
+ (x) => (ts ? x.presence.isDeclared(ts) || x.presence.undeclaredGroups.includes(ts) : true) && (!inst || x.presence.openRecords(ts).some((o) => o.instance === inst)),
436
528
  ts ? `a toolset "${ts}"${inst ? ` with instance "${inst}"` : ''}` : `an open instance "${inst}"`,
437
529
  );
438
530
  return this.lookNow(args, c.serverId, tool, mark);
@@ -442,14 +534,14 @@ export class ToolsetsModule implements Module {
442
534
  return textResult(all.map((c) => c.presence.catalog({ mark })).join('\n\n'));
443
535
  }
444
536
  const c = pick(this.hub, server);
445
- if (tool) return this.lookTool(c, tool, str(args.instance));
537
+ if (tool) return this.lookTool(c, tool, instanceNamed([c], c.presence.toolsetOf(tool), args.instance));
446
538
  const p = c.presence;
447
- let toolset = str(args.toolset);
448
- const instance = str(args.instance);
539
+ let toolset = toolsetNamed(this.hub, server, args.toolset);
540
+ const instance = instanceNamed([c], toolset, args.instance);
449
541
  if (!toolset && instance) {
450
542
  const open = p.openRecords().filter((o) => o.instance !== undefined);
451
543
  toolset = open.find((o) => o.instance === instance)?.toolset
452
- ?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${o.toolset}:${o.instance}`).join(', ')}` : 'no instance is open there'}`);
544
+ ?? fail(`no open instance "${instance}" on ${c.serverId}; ${open.length ? `open: ${open.map((o) => `${nameToTypeBack(o.toolset)}:${nameToTypeBack(String(o.instance))}`).join(', ')}` : 'no instance is open there'}`);
453
545
  }
454
546
  if (!toolset) return textResult(p.catalog({ mark }));
455
547
  if (p.isOpen(toolset, instance) || (!instance && p.openRecords(toolset).length === 1)) {
@@ -464,8 +556,8 @@ export class ToolsetsModule implements Module {
464
556
  if (d) {
465
557
  const open = p.openRecords(toolset);
466
558
  return textResult([
467
- `${c.serverId}/${toolset}${d.title ? ` — ${d.title}` : ''}${d.summary ? `: ${d.summary}` : ''}`,
468
- open.length ? `open: ${open.map((o) => o.instance ?? toolset).join(', ')}` : 'not open',
559
+ `${c.serverId}/${nameToTypeBack(toolset)}${d.title ? ` — ${suppliedValue(d.title)}` : ''}${d.summary ? `: ${suppliedValue(d.summary)}` : ''}`,
560
+ open.length ? `open: ${open.map((o) => nameToTypeBack(o.instance ?? toolset)).join(', ')}` : 'not open',
469
561
  ...(d.instances ? ['several can be open at once; name each with instance'] : []),
470
562
  ...(d.options ? [`options: ${jsonText(d.options)}`] : []),
471
563
  `${p.openHint(toolset)} opens it.`,
@@ -473,7 +565,7 @@ export class ToolsetsModule implements Module {
473
565
  }
474
566
  if (p.undeclaredGroups.includes(toolset)) {
475
567
  const names = p.allTools.filter((t) => p.toolsetOf(t.name) === toolset).map((t) => t.name);
476
- return textResult(`${c.serverId}/${toolset}: ${names.join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
568
+ return textResult(`${c.serverId}/${nameToTypeBack(toolset)}: ${names.map(nameToTypeBack).join(', ')}; not open: ${p.openHint(toolset)} opens it.`);
477
569
  }
478
570
  return fail(noToolset(c, toolset!));
479
571
  }
@@ -491,13 +583,13 @@ export class ToolsetsModule implements Module {
491
583
 
492
584
  private async close(args: Record<string, unknown>, take: ((taken: TakenChange) => void) | null): Promise<ToolResult> {
493
585
  const shown = take !== null;
494
- const named = str(args.toolset);
586
+ const named = toolsetNamed(this.hub, args.server, args.toolset);
495
587
  const c = named && !str(args.server)
496
- ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, str(args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
588
+ ? owner(this.hub, undefined, (x) => x.presence.isOpen(named, instanceNamed([x], named, args.instance)) || x.presence.openRecords(named).length > 0, `an open toolset "${named}"`)
497
589
  : pick(this.hub, args.server);
498
590
  const p = c.presence;
499
591
  let toolset = named;
500
- let instance = str(args.instance);
592
+ let instance = instanceNamed([c], named, args.instance);
501
593
  if (!toolset) {
502
594
  const open = [
503
595
  ...p.openRecords().filter((o) => !instance || o.instance === instance),
@@ -505,7 +597,7 @@ export class ToolsetsModule implements Module {
505
597
  ];
506
598
  if (open.length !== 1) {
507
599
  fail(open.length
508
- ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${o.toolset}${o.instance ? `:${o.instance}` : ''}`).join(', ')}`
600
+ ? `several are open on ${c.serverId}: name one of ${open.map((o) => `${nameToTypeBack(o.toolset)}${o.instance ? `:${nameToTypeBack(o.instance)}` : ''}`).join(', ')}`
509
601
  : `nothing is open on ${c.serverId}`);
510
602
  }
511
603
  toolset = open[0]!.toolset;
@@ -513,17 +605,17 @@ export class ToolsetsModule implements Module {
513
605
  }
514
606
  if (p.isDeclared(toolset!)) {
515
607
  // As open refuses one: a toolset without instances is closed without one.
516
- if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${toolset} has no instances: close it without one`);
608
+ if (instance && !p.declared[toolset!]!.instances) fail(`${c.serverId}/${nameToTypeBack(toolset!)} has no instances: close it without one`);
517
609
  if (p.declared[toolset!]!.instances && !instance) {
518
610
  const open = p.openRecords(toolset);
519
- if (open.length !== 1) fail(open.length ? `several ${toolset} instances are open (${open.map((o) => o.instance).join(', ')}): say which` : `${toolset} is not open`);
611
+ if (open.length !== 1) fail(open.length ? `several ${nameToTypeBack(toolset!)} instances are open (${open.map((o) => nameToTypeBack(String(o.instance))).join(', ')}): say which` : `${nameToTypeBack(toolset!)} is not open`);
520
612
  instance = open[0]!.instance;
521
613
  }
522
614
  let closed: Awaited<ReturnType<ToolsetController['close']>>;
523
615
  try {
524
616
  closed = await c.close(toolset!, instance, { shown });
525
617
  } catch (error) {
526
- const what = `${toolset}${instance ? `:${instance}` : ''}`;
618
+ const what = `${nameToTypeBack(toolset!)}${instance ? `:${nameToTypeBack(instance)}` : ''}`;
527
619
  return requestFailure(c.serverId, `did not close ${what}`, `didn't confirm closing ${what}`, error);
528
620
  }
529
621
  const key = keyOf(toolset!, instance);
@@ -532,8 +624,8 @@ export class ToolsetsModule implements Module {
532
624
  return blocksResult([...asBlocks(closed.content), { type: 'text', text: change }]);
533
625
  }
534
626
  if (p.undeclaredGroups.includes(toolset!)) {
535
- if (instance) fail(`${c.serverId}/${toolset} takes no instance (the server doesn't declare it)`);
536
- if (!p.isOpen(toolset!)) fail(`${c.serverId}/${toolset} is not open`);
627
+ if (instance) fail(`${c.serverId}/${nameToTypeBack(toolset!)} takes no instance (the server doesn't declare it)`);
628
+ if (!p.isOpen(toolset!)) fail(`${c.serverId}/${nameToTypeBack(toolset!)} is not open`);
537
629
  const cs = c.setGroupOpen(toolset!, false);
538
630
  const text = p.render(cs, { mark: shown, describeAll: !shown }) || `${p.label(keyOf(toolset!))} closed.`;
539
631
  if (take) take({ toolsets: c, cs });
@@ -19,9 +19,10 @@
19
19
  */
20
20
 
21
21
  import { spawn, type ChildProcess } from 'node:child_process';
22
- import { createInterface, type Interface as ReadlineInterface } from 'node:readline';
23
22
  import { EventEmitter } from 'node:events';
23
+ import type { Readable } from 'node:stream';
24
24
  import { StringDecoder } from 'node:string_decoder';
25
+ import { escapeLineSeparators, readLines } from '../line-reader.js';
25
26
  import WebSocket from 'ws';
26
27
 
27
28
  import type { McplServerConfig } from './types.js';
@@ -325,16 +326,18 @@ export class StdioTransport extends McplTransport {
325
326
  });
326
327
  }
327
328
 
329
+ private readonly stopLines: () => void;
330
+
328
331
  private constructor(
329
332
  private readonly child: ChildProcess,
330
- private readonly rl: ReadlineInterface,
333
+ stdout: Readable,
331
334
  /** The server this child serves, named in its stderr lines and in what
332
335
  * its reaping reports. */
333
336
  serverId: string,
334
337
  ) {
335
338
  super(serverId);
336
339
 
337
- this.rl.on('line', (line: string) => this.pushLine(line));
340
+ this.stopLines = readLines(stdout, (line) => this.pushLine(line));
338
341
 
339
342
  // Stitch stderr chunks into whole lines (a chunk can split a line, and a
340
343
  // character's bytes), and keep the stream's last characters for
@@ -383,12 +386,7 @@ export class StdioTransport extends McplTransport {
383
386
  });
384
387
  };
385
388
  this.child.stdin?.on('error', endOnPipeError);
386
- // On the stream itself, since close() closes readline, which takes its own
387
- // listener off the stream. While readline is open, its listener, which
388
- // came first, re-emits the error on the interface, where an empty
389
- // listener absorbs that copy.
390
389
  this.child.stdout?.on('error', endOnPipeError);
391
- this.rl.on('error', () => { /* readline's copy of a stdout error, handled above */ });
392
390
  // stderr carries diagnostics only: a read error on it is reported, to the
393
391
  // listeners or else in the log, and never thrown. It ends nothing once
394
392
  // the connection is wired; during a handshake the listener is the
@@ -453,8 +451,7 @@ export class StdioTransport extends McplTransport {
453
451
  resolve();
454
452
  });
455
453
  });
456
- const rl = createInterface({ input: child.stdout! });
457
- return new StdioTransport(child, rl, config.id);
454
+ return new StdioTransport(child, child.stdout!, config.id);
458
455
  }
459
456
 
460
457
  writeLine(json: string): void {
@@ -462,7 +459,7 @@ export class StdioTransport extends McplTransport {
462
459
  // transport is ending it.
463
460
  if (this.pipeError) return;
464
461
  // stdin may be gone if the child died between the closed-check and here.
465
- this.child.stdin?.write(json + '\n');
462
+ this.child.stdin?.write(escapeLineSeparators(json) + '\n');
466
463
  }
467
464
 
468
465
  private closing: Promise<void> | null = null;
@@ -492,7 +489,7 @@ export class StdioTransport extends McplTransport {
492
489
 
493
490
  private async reap(): Promise<void> {
494
491
  this.markClosed({ reason: 'closed by host' });
495
- this.rl.close();
492
+ this.stopLines();
496
493
  await this.terminate();
497
494
  }
498
495
 
@@ -696,7 +693,7 @@ export class WebSocketTransport extends McplTransport {
696
693
 
697
694
  writeLine(json: string): void {
698
695
  if (this.ws.readyState === WebSocket.OPEN) {
699
- this.ws.send(json);
696
+ this.ws.send(escapeLineSeparators(json));
700
697
  }
701
698
  }
702
699
 
@@ -830,7 +827,7 @@ const SERVER_CONFIG_BOOLEANS: Record<BooleanSetting, string> = {
830
827
  inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
831
828
  reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
832
829
  allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
833
- toolHints: 'true advertises MCPL tool hints (RFC-014), so the server\'s dynamic tools are announced in the conversation and called through toolsets--act, and false or leaving it out lists every tool as before',
830
+ toolHints: 'true advertises MCPL tool hints (RFC-014) to an MCPL server, so its dynamic tools are announced in the conversation and called through toolsets--act; leaving it out does the same wherever the name toolsets is free; false lists every tool as before and advertises none',
834
831
  };
835
832
 
836
833
  /**
@@ -849,6 +846,13 @@ export function assertServerConfigBooleans(config: McplServerConfig): void {
849
846
  if (setting === 'inheritEnv' && dialsNetwork(config)) continue;
850
847
  assertBooleanSetting(`MCPL server "${config.id}" ${setting}`, config[setting], whatTrueDoes);
851
848
  }
849
+ // Not a boolean, but read the same way: a value that isn't one of the
850
+ // three would otherwise be taken for the default.
851
+ const presentation = (config as { presentation?: unknown }).presentation;
852
+ if (presentation !== undefined && presentation !== null && presentation !== 'full' && presentation !== 'tab' && presentation !== 'closed-tab') {
853
+ const kind = typeof presentation === 'string' ? 'another string' : Array.isArray(presentation) ? 'a list' : typeof presentation;
854
+ throw new Error(`MCPL server "${config.id}" presentation must be "full", "tab" or "closed-tab" (or left out for full), not ${kind}`);
855
+ }
852
856
  }
853
857
 
854
858
  /**