@frockbot/plugin-tools 0.3.10 → 0.3.12

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-tools",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -17,7 +17,7 @@
17
17
  "typecheck": "tsc --noEmit -p tsconfig.json"
18
18
  },
19
19
  "dependencies": {
20
- "@frockbot/kernel-contracts": "0.3.10",
20
+ "@frockbot/kernel-contracts": "0.3.12",
21
21
  "cordis": "4.0.0-rc.8"
22
22
  },
23
23
  "devDependencies": {
@@ -591,6 +591,64 @@ describe("progressive tool disclosure", () => {
591
591
  });
592
592
  });
593
593
 
594
+ describe("the envelope discovery hands back", () => {
595
+ test("is the envelope dispatch accepts, for a non-external namespace", async () => {
596
+ // The Applets Package mounts through the isolate host, whose namespaces
597
+ // used to register `external: true`. The dispatch guard then refused every
598
+ // call without `mcpDetails.description`, while discovery's `callWith`
599
+ // never mentioned the field — so the envelope offered was the envelope
600
+ // refused, and no Applet could be created by chat at all.
601
+ const root = await rootWithTools();
602
+ root.tools.register(dynamicTool("applets", "applet_create"));
603
+ root.tools.registerNamespace({ name: "applets", status: "ready" });
604
+
605
+ const discovered = await invoke(root, GET_DYNAMIC_TOOLS_NAME, {
606
+ namespace: "applets",
607
+ toolName: "applet_create",
608
+ });
609
+ const envelope = JSON.parse(discovered.content as string).callWith as {
610
+ tool: string;
611
+ input: Record<string, unknown>;
612
+ };
613
+ expect(envelope.tool).toBe(CALL_DYNAMIC_TOOL_NAME);
614
+ expect(envelope.input.mcpDetails).toBeUndefined();
615
+
616
+ // The exact envelope, with only the placeholder arguments made real.
617
+ const dispatched = await invoke(root, CALL_DYNAMIC_TOOL_NAME, {
618
+ ...envelope.input,
619
+ arguments: { value: "a todo list" },
620
+ });
621
+
622
+ expect(dispatched.isError).toBe(false);
623
+ expect(dispatched.content).toContain("a todo list");
624
+ });
625
+
626
+ test("carries mcpDetails when the namespace really is external", async () => {
627
+ const root = await rootWithTools();
628
+ root.tools.register(dynamicTool("linear", "issue_create"));
629
+ root.tools.registerNamespace({ name: "linear", external: true });
630
+
631
+ const discovered = await invoke(root, GET_DYNAMIC_TOOLS_NAME, {
632
+ namespace: "linear",
633
+ toolName: "issue_create",
634
+ });
635
+ const envelope = JSON.parse(discovered.content as string).callWith as {
636
+ input: Record<string, unknown>;
637
+ };
638
+ expect(envelope.input.mcpDetails).toEqual({
639
+ description: "<one sentence saying why you are calling this>",
640
+ });
641
+
642
+ const dispatched = await invoke(root, CALL_DYNAMIC_TOOL_NAME, {
643
+ ...envelope.input,
644
+ arguments: { value: "an issue" },
645
+ mcpDetails: { description: "Filing the bug the User just described." },
646
+ });
647
+
648
+ expect(dispatched.isError).toBe(false);
649
+ });
650
+ });
651
+
594
652
  describe("a dynamic tool called by its bare name", () => {
595
653
  test("is answered with the envelope, not a dead-end Unknown tool", async () => {
596
654
  // `applet_list` is listed to the model by bare name in
@@ -623,6 +681,17 @@ describe("a dynamic tool called by its bare name", () => {
623
681
  expect(result.content).toContain("mail-two");
624
682
  });
625
683
 
684
+ test("routes an external namespace with the mcpDetails its guard demands", async () => {
685
+ const root = await rootWithTools();
686
+ root.tools.register(dynamicTool("linear", "issue_create"));
687
+ root.tools.registerNamespace({ name: "linear", external: true });
688
+
689
+ const result = await invoke(root, "issue_create", {});
690
+
691
+ expect(result.content).toContain("mcpDetails");
692
+ expect(result.content).toContain("description");
693
+ });
694
+
626
695
  test("a name in no namespace still says only that it is unknown", async () => {
627
696
  const root = await rootWithTools();
628
697
  root.tools.register(dynamicTool("applets", "applet_list"));
package/src/tools.ts CHANGED
@@ -573,7 +573,14 @@ export class ToolRegistry extends Service implements ToolExecution {
573
573
  namespaces.length === 1
574
574
  ? `namespace "${first}"`
575
575
  : `namespaces ${listOrNone(namespaces)} — pick one`;
576
- return `Unknown tool: ${name}. It is a dynamic tool in ${where}; call it through ${CALL_DYNAMIC_TOOL_NAME} as {"namespace":"${first}","toolName":"${name}","arguments":{ … the tool's own input … }}. Call ${GET_DYNAMIC_TOOLS_NAME}({"namespace":"${first}","toolName":"${name}"}) first if you do not have its schema.`;
576
+ // Same rule as the discovery envelope: an external namespace's dispatch
577
+ // guard refuses a call without `mcpDetails.description`, so a route that
578
+ // omits it just moves the dead end one step later.
579
+ const details =
580
+ this.namespaces.get(first)?.external === true
581
+ ? `,"mcpDetails":{"description":"<one sentence saying why you are calling this>"}`
582
+ : "";
583
+ return `Unknown tool: ${name}. It is a dynamic tool in ${where}; call it through ${CALL_DYNAMIC_TOOL_NAME} as {"namespace":"${first}","toolName":"${name}","arguments":{ … the tool's own input … }${details}}. Call ${GET_DYNAMIC_TOOLS_NAME}({"namespace":"${first}","toolName":"${name}"}) first if you do not have its schema.`;
577
584
  }
578
585
 
579
586
  private async prepareRegistered(
@@ -990,6 +997,20 @@ export class ToolRegistry extends Service implements ToolExecution {
990
997
  namespace: namespaceName,
991
998
  toolName: registered.definition.name,
992
999
  arguments: "<an object matching inputSchema>",
1000
+ // The envelope handed back has to be the envelope accepted. An
1001
+ // external namespace's dispatch guard refuses a call without
1002
+ // `mcpDetails.description`, and a model that copied this
1003
+ // envelope verbatim was denied for a field the envelope never
1004
+ // mentioned.
1005
+ ...(this.namespaces.get(registered.definition.namespace ?? "")
1006
+ ?.external === true
1007
+ ? {
1008
+ mcpDetails: {
1009
+ description:
1010
+ "<one sentence saying why you are calling this>",
1011
+ },
1012
+ }
1013
+ : {}),
993
1014
  },
994
1015
  },
995
1016
  }),