@jr2/orchestrator 0.5.1 → 0.7.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/console/components/app.ts +55 -15
- package/console/components/bottom-drawer.ts +259 -0
- package/console/components/drawer.ts +14 -6
- package/console/components/fleet.ts +36 -13
- package/console/components/machine-pane.ts +61 -5
- package/console/components/nav.ts +54 -8
- package/console/conversation.ts +161 -0
- package/console/login.ts +123 -0
- package/console/main.ts +600 -60
- package/console/store.ts +611 -64
- package/console/style.css +231 -0
- package/docs/agents-block.md +2 -2
- package/docs/concepts/agents-and-turns.md +142 -29
- package/docs/concepts/architecture.md +60 -34
- package/docs/concepts/callers-and-hooks.md +205 -0
- package/docs/concepts/charts.md +13 -8
- package/docs/concepts/composition.md +10 -1
- package/docs/concepts/converge.md +7 -2
- package/docs/concepts/durable-runs.md +43 -16
- package/docs/concepts/effects.md +133 -50
- package/docs/concepts/gates.md +95 -25
- package/docs/concepts/machines-and-workflows.md +14 -4
- package/docs/concepts/pools-and-sources.md +193 -17
- package/docs/concepts/secrets-and-trust.md +186 -62
- package/docs/concepts/the-menu.md +14 -0
- package/docs/concepts/workspaces.md +135 -56
- package/docs/get-started/a-model-decides.md +14 -13
- package/docs/get-started/an-agent-in-a-sandbox.md +46 -35
- package/docs/get-started/connect-a-model.md +1 -1
- package/docs/get-started/create-an-instance.md +17 -14
- package/docs/get-started/have-your-assistant-write-a-machine.md +111 -40
- package/docs/get-started/install.md +4 -0
- package/docs/guides/add-tools-to-the-sandbox.md +72 -15
- package/docs/guides/answer-a-gate-from-a-forge.md +304 -0
- package/docs/guides/answer-a-gate.md +74 -37
- package/docs/guides/bind-open-parts.md +29 -12
- package/docs/guides/call-an-mcp-tool-from-a-machine.md +572 -0
- package/docs/guides/cancel-a-run.md +26 -16
- package/docs/guides/cut-a-worktree.md +560 -0
- package/docs/guides/fix-a-failed-converge.md +19 -7
- package/docs/guides/give-a-caller-a-scope.md +203 -0
- package/docs/guides/give-an-agent-mcp-tools.md +422 -0
- package/docs/guides/iterate-on-a-machine-on-kind.md +10 -9
- package/docs/guides/let-an-agent-write-a-chart.md +14 -7
- package/docs/guides/look-inside-a-sandbox.md +115 -42
- package/docs/guides/move-from-kind-to-a-shared-cluster.md +16 -0
- package/docs/guides/open-the-console.md +63 -14
- package/docs/guides/push-and-sign-a-branch.md +67 -21
- package/docs/guides/read-a-conversation.md +225 -0
- package/docs/guides/run-a-chart-by-name.md +17 -16
- package/docs/guides/run-a-command-or-call-a-url.md +84 -41
- package/docs/guides/run-one-worker-per-item.md +34 -27
- package/docs/guides/set-an-agents-access-and-dials.md +69 -22
- package/docs/guides/size-a-workspace.md +19 -6
- package/docs/guides/start-a-run-for-each-alert.md +290 -0
- package/docs/guides/start-a-run-from-a-run.md +18 -16
- package/docs/guides/start-a-run.md +24 -18
- package/docs/guides/steer-an-agent.md +332 -0
- package/docs/guides/use-a-hosted-provider.md +7 -1
- package/docs/guides/watch-a-run.md +146 -117
- package/docs/guides/write-a-hook-kind.md +238 -0
- package/docs/guides/write-a-tool-as-an-mcp-server.md +351 -0
- package/docs/guides/write-a-workflow.md +18 -8
- package/docs/llms.txt +110 -23
- package/docs/reference/authoring-surface/agent.md +35 -12
- package/docs/reference/authoring-surface/answer.md +20 -4
- package/docs/reference/authoring-surface/arg.md +28 -6
- package/docs/reference/authoring-surface/basic.md +1 -1
- package/docs/reference/authoring-surface/bearer.md +1 -1
- package/docs/reference/authoring-surface/branchname.md +15 -0
- package/docs/reference/authoring-surface/carry.md +23 -0
- package/docs/reference/authoring-surface/chartschema.md +1 -1
- package/docs/reference/authoring-surface/chartschemafor.md +1 -1
- package/docs/reference/authoring-surface/command.md +37 -22
- package/docs/reference/authoring-surface/customize.md +4 -1
- package/docs/reference/authoring-surface/defineconfig.md +1 -1
- package/docs/reference/authoring-surface/defineevent.md +1 -1
- package/docs/reference/authoring-surface/dispatch.md +5 -4
- package/docs/reference/authoring-surface/fail.md +18 -5
- package/docs/reference/authoring-surface/file.md +37 -17
- package/docs/reference/authoring-surface/frompick.md +1 -1
- package/docs/reference/authoring-surface/gate.md +118 -0
- package/docs/reference/authoring-surface/group-agents.md +15 -0
- package/docs/reference/authoring-surface/group-charts.md +16 -0
- package/docs/reference/authoring-surface/group-composition.md +25 -0
- package/docs/reference/authoring-surface/group-config.md +17 -0
- package/docs/reference/authoring-surface/group-effects.md +48 -0
- package/docs/reference/authoring-surface/group-failure.md +13 -0
- package/docs/reference/authoring-surface/group-gates.md +16 -0
- package/docs/reference/authoring-surface/group-machine-and-events.md +15 -0
- package/docs/reference/authoring-surface/group-steering.md +12 -0
- package/docs/reference/authoring-surface/group-types-these-signatures-name.md +137 -0
- package/docs/reference/authoring-surface/group-workspaces-and-repos.md +15 -0
- package/docs/reference/authoring-surface/http.md +109 -9
- package/docs/reference/authoring-surface/isfailure.md +1 -1
- package/docs/reference/authoring-surface/isready.md +14 -0
- package/docs/reference/authoring-surface/jr2setup.md +14 -4
- package/docs/reference/authoring-surface/mcp.md +107 -0
- package/docs/reference/authoring-surface/open.md +41 -8
- package/docs/reference/authoring-surface/openbody.md +1 -1
- package/docs/reference/authoring-surface/paramsof.md +23 -5
- package/docs/reference/authoring-surface/pool.md +79 -11
- package/docs/reference/authoring-surface/push.md +31 -13
- package/docs/reference/authoring-surface/readyof.md +21 -0
- package/docs/reference/authoring-surface/reread.md +19 -0
- package/docs/reference/authoring-surface/secret.md +26 -5
- package/docs/reference/authoring-surface/sentby.md +1 -1
- package/docs/reference/authoring-surface/skip.md +22 -0
- package/docs/reference/authoring-surface/source.md +59 -4
- package/docs/reference/authoring-surface/steer.md +93 -0
- package/docs/reference/authoring-surface/text.md +1 -1
- package/docs/reference/authoring-surface/type-agentdefinition.md +16 -3
- package/docs/reference/authoring-surface/type-agentmcpserver.md +54 -0
- package/docs/reference/authoring-surface/type-agentslots.md +4 -1
- package/docs/reference/authoring-surface/type-anyframepath.md +14 -0
- package/docs/reference/authoring-surface/type-anyworktreepath.md +15 -0
- package/docs/reference/authoring-surface/type-bindactors.md +14 -0
- package/docs/reference/authoring-surface/type-binding.md +6 -7
- package/docs/reference/authoring-surface/type-caller.md +40 -0
- package/docs/reference/authoring-surface/type-callerrecord.md +32 -0
- package/docs/reference/authoring-surface/type-carry.md +19 -0
- package/docs/reference/authoring-surface/type-chainlink.md +3 -3
- package/docs/reference/authoring-surface/type-commandinput.md +4 -3
- package/docs/reference/authoring-surface/type-commandoptions.md +0 -22
- package/docs/reference/authoring-surface/type-commandoutput.md +23 -5
- package/docs/reference/authoring-surface/type-customize.md +20 -3
- package/docs/reference/authoring-surface/type-delivered.md +17 -0
- package/docs/reference/authoring-surface/type-deliveredevent.md +18 -0
- package/docs/reference/authoring-surface/type-dispatchdefinition.md +4 -3
- package/docs/reference/authoring-surface/type-dispatchoutput.md +9 -2
- package/docs/reference/authoring-surface/type-doorlessinput.md +19 -0
- package/docs/reference/authoring-surface/type-effectbinding.md +12 -2
- package/docs/reference/authoring-surface/type-filebody.md +4 -8
- package/docs/reference/authoring-surface/type-framepath.md +14 -0
- package/docs/reference/authoring-surface/type-gatebinding.md +15 -0
- package/docs/reference/authoring-surface/type-gatehooks.md +12 -0
- package/docs/reference/authoring-surface/type-gatelook.md +37 -0
- package/docs/reference/authoring-surface/type-gateoutcome.md +32 -0
- package/docs/reference/authoring-surface/type-gatereply.md +19 -0
- package/docs/reference/authoring-surface/type-gateslot.md +21 -0
- package/docs/reference/authoring-surface/type-gateslotinput.md +13 -0
- package/docs/reference/authoring-surface/type-gateslotparts.md +18 -0
- package/docs/reference/authoring-surface/type-gateslots.md +12 -0
- package/docs/reference/authoring-surface/type-gatetypesat.md +13 -0
- package/docs/reference/authoring-surface/type-hostinjectedinput.md +3 -5
- package/docs/reference/authoring-surface/type-httpdefinition.md +6 -3
- package/docs/reference/authoring-surface/type-httpfileinput.md +20 -0
- package/docs/reference/authoring-surface/type-httpoutput.md +8 -2
- package/docs/reference/authoring-surface/type-jr2config.md +37 -2
- package/docs/reference/authoring-surface/type-lateroutcome.md +21 -0
- package/docs/reference/authoring-surface/type-lends.md +5 -1
- package/docs/reference/authoring-surface/type-lookoutput.md +14 -0
- package/docs/reference/authoring-surface/type-mcpcalloutput.md +78 -0
- package/docs/reference/authoring-surface/type-mcpnotreturned.md +42 -0
- package/docs/reference/authoring-surface/type-mcpoptions.md +47 -0
- package/docs/reference/authoring-surface/type-mcpoutput.md +50 -0
- package/docs/reference/authoring-surface/type-mcpreadoutput.md +53 -0
- package/docs/reference/authoring-surface/type-mcpremoteserver.md +50 -0
- package/docs/reference/authoring-surface/type-mcpserver.md +41 -0
- package/docs/reference/authoring-surface/type-mcpstdioserver.md +42 -0
- package/docs/reference/authoring-surface/type-namedsecret.md +4 -4
- package/docs/reference/authoring-surface/type-namestart.md +77 -0
- package/docs/reference/authoring-surface/type-noslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-notworktreekeys.md +12 -0
- package/docs/reference/authoring-surface/type-openbodydeclaration.md +1 -1
- package/docs/reference/authoring-surface/type-openbodyoutput.md +1 -1
- package/docs/reference/authoring-surface/type-opencommandinput.md +7 -2
- package/docs/reference/authoring-surface/type-permissivenoslotworkspaceoptions.md +1 -6
- package/docs/reference/authoring-surface/type-permissiveworkspaceoptions.md +5 -10
- package/docs/reference/authoring-surface/type-poolnextinput.md +22 -0
- package/docs/reference/authoring-surface/type-poolspec.md +11 -6
- package/docs/reference/authoring-surface/type-pushinput.md +22 -3
- package/docs/reference/authoring-surface/type-pushoutput.md +33 -8
- package/docs/reference/authoring-surface/type-pushsourcespec.md +38 -0
- package/docs/reference/authoring-surface/type-readsourcespec.md +47 -0
- package/docs/reference/authoring-surface/type-reposlot.md +6 -6
- package/docs/reference/authoring-surface/type-reread.md +18 -0
- package/docs/reference/authoring-surface/type-runsconfig.md +41 -0
- package/docs/reference/authoring-surface/type-sandboxoptions.md +11 -4
- package/docs/reference/authoring-surface/type-sender.md +26 -1
- package/docs/reference/authoring-surface/type-sendercallerfield.md +14 -0
- package/docs/reference/authoring-surface/type-size.md +1 -1
- package/docs/reference/authoring-surface/type-skip.md +19 -0
- package/docs/reference/authoring-surface/type-slothandles.md +5 -13
- package/docs/reference/authoring-surface/type-sourcehooks.md +23 -0
- package/docs/reference/authoring-surface/type-sourcein.md +25 -0
- package/docs/reference/authoring-surface/type-sourcereply.md +18 -0
- package/docs/reference/authoring-surface/type-sourcespec.md +26 -17
- package/docs/reference/authoring-surface/type-sourceverdict.md +49 -0
- package/docs/reference/authoring-surface/type-standingpoolmachine.md +12 -0
- package/docs/reference/authoring-surface/type-standingpoolspec.md +51 -0
- package/docs/reference/authoring-surface/type-steerargs.md +20 -0
- package/docs/reference/authoring-surface/type-thinkinglevel.md +1 -1
- package/docs/reference/authoring-surface/type-usercontainer.md +1 -1
- package/docs/reference/authoring-surface/type-workspaced.md +30 -9
- package/docs/reference/authoring-surface/type-workspacehandles.md +5 -6
- package/docs/reference/authoring-surface/type-workspaceoptions.md +1 -13
- package/docs/reference/authoring-surface/type-worktreefound.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeinput.md +56 -0
- package/docs/reference/authoring-surface/type-worktreekey.md +14 -0
- package/docs/reference/authoring-surface/type-worktreeoptions.md +29 -0
- package/docs/reference/authoring-surface/type-worktreeoutput.md +45 -0
- package/docs/reference/authoring-surface/type-worktreepath.md +16 -0
- package/docs/reference/authoring-surface/type-worktreeready.md +37 -0
- package/docs/reference/authoring-surface/type-worktreerefused.md +21 -0
- package/docs/reference/authoring-surface/workspace.md +66 -38
- package/docs/reference/authoring-surface/worktree.md +71 -0
- package/docs/reference/authoring-surface.md +88 -147
- package/docs/reference/cli.md +413 -74
- package/docs/reference/config.md +248 -6
- package/docs/reference/decisions/0001-operator-owned-generic-sandbox.md +10 -9
- package/docs/reference/decisions/0002-duplex-streaming-actor.md +81 -14
- package/docs/reference/decisions/0004-worktree-provisioning-model.md +27 -21
- package/docs/reference/decisions/0005-sandbox-pod-composition.md +39 -25
- package/docs/reference/decisions/0006-agent-control-surface.md +6 -3
- package/docs/reference/decisions/0007-durable-machine-state.md +1 -1
- package/docs/reference/decisions/0009-cli-and-instance-interface.md +150 -49
- package/docs/reference/decisions/0010-bdd-acceptance-tests.md +5 -3
- package/docs/reference/decisions/0011-workflow-defined-events.md +256 -12
- package/docs/reference/decisions/0012-workspace-wrapper-machine.md +39 -32
- package/docs/reference/decisions/0013-the-agent-reaches-its-machine-through-a-container-it-cannot-read.md +54 -30
- package/docs/reference/decisions/0014-observation-is-open-run-state-is-not.md +53 -27
- package/docs/reference/decisions/0015-authoring-surface-absorbs-the-mechanism.md +26 -16
- package/docs/reference/decisions/0016-agent-turn-mechanics-are-internal.md +9 -7
- package/docs/reference/decisions/0017-pool-over-a-source-port.md +226 -10
- package/docs/reference/decisions/0018-instance-agents-are-definitions-jr2-assembles-the-harness.md +15 -7
- package/docs/reference/decisions/0019-one-converging-command-against-the-current-context.md +53 -27
- package/docs/reference/decisions/0021-workspace-continuity-is-a-lease-that-answers-back.md +16 -2
- package/docs/reference/decisions/0022-observation-is-a-level-triggered-feed.md +4 -4
- package/docs/reference/decisions/0023-the-harness-prints-the-conversation.md +124 -43
- package/docs/reference/decisions/0024-an-agents-turn-ends-with-the-state-that-asked-for-it.md +36 -38
- package/docs/reference/decisions/0025-cancel-ends-a-run-stop-parks-it.md +10 -5
- package/docs/reference/decisions/0027-the-harness-is-jr2s-own-server-flue-retires-the-wire-stays.md +16 -7
- package/docs/reference/decisions/0028-what-an-agent-may-do-to-the-workspace-is-part-of-its-definition.md +36 -19
- package/docs/reference/decisions/0029-a-menu-is-fixed-for-a-conversation-and-a-turn-is-told-its-allowed-picks.md +18 -10
- package/docs/reference/decisions/0030-a-snapshot-names-the-machine-it-was-written-under.md +3 -2
- package/docs/reference/decisions/0031-menu-only-agents-run-on-the-instance-harness.md +23 -15
- package/docs/reference/decisions/0032-the-console-unlocks-with-a-callers-credential.md +101 -0
- package/docs/reference/decisions/0033-a-machine-declares-the-input-that-starts-it.md +22 -22
- package/docs/reference/decisions/0034-the-console-renders-with-preact-and-the-server-erases-the-types.md +6 -6
- package/docs/reference/decisions/0037-an-instance-builds-its-sandbox-images-jr2-injects-the-harness.md +13 -12
- package/docs/reference/decisions/0049-a-machine-carries-its-parts-and-composes-by-invoke.md +23 -23
- package/docs/reference/decisions/0050-what-code-names-config-declares-what-the-cli-names-discovery-finds.md +6 -1
- package/docs/reference/decisions/0051-a-repo-is-a-slot-on-the-workspace-and-a-cache-on-the-node.md +57 -46
- package/docs/reference/decisions/0052-a-sandbox-lands-where-an-ordinary-pod-lands-and-the-cache-follows-it.md +5 -0
- package/docs/reference/decisions/0053-a-fetch-inside-the-pod-asks-the-node-cache-and-the-cache-asks-the-remote.md +20 -8
- package/docs/reference/decisions/0054-the-kit-ships-machines-and-a-shipped-machine-leaves-its-parts-open.md +55 -40
- package/docs/reference/decisions/0055-a-release-is-a-pushed-tag-and-the-publish-guard-is-the-credential.md +14 -13
- package/docs/reference/decisions/0057-a-turn-is-its-frame-its-dials-and-whether-it-continues.md +31 -24
- package/docs/reference/decisions/0058-a-harness-answers-the-orchestrator-alone.md +18 -10
- package/docs/reference/decisions/0059-a-harness-holds-stand-ins-and-the-custodian-holds-the-keys.md +137 -86
- package/docs/reference/decisions/0062-jr2-briefs-the-agent-on-its-seat-never-on-its-task.md +165 -91
- package/docs/reference/decisions/0063-the-orchestrator-watches-the-cluster.md +7 -7
- package/docs/reference/decisions/0064-a-workspace-waits-for-capacity.md +2 -2
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-kept-for-a-set-time.md +109 -0
- package/docs/reference/decisions/0066-a-model-tier-runs-a-real-model-and-a-change-to-what-it-reads-runs-the-tier.md +53 -23
- package/docs/reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md +221 -153
- package/docs/reference/decisions/0068-an-effector-holds-an-effects-secrets-and-signs-its-wire.md +161 -96
- package/docs/reference/decisions/0069-the-orchestrator-is-a-kit-image-and-an-instance-publishes-its-assembly.md +32 -9
- package/docs/reference/decisions/0070-a-machine-lends-its-parts-to-a-chart.md +48 -33
- package/docs/reference/decisions/0071-a-run-starts-a-run-through-a-dispatch-effect.md +56 -30
- package/docs/reference/decisions/0072-the-docs-ship-inside-the-kit-and-an-instances-agents-md-indexes-them.md +35 -25
- package/docs/reference/decisions/0073-a-machine-fails-a-run-with-fail-and-an-uncaught-failure-ends-it-failed.md +1 -1
- package/docs/reference/decisions/0075-a-machine-cuts-a-worktree-and-the-workspace-only-clones.md +296 -0
- package/docs/reference/decisions/0076-an-agents-control-credential-lives-only-where-no-workspace-code-runs.md +347 -0
- package/docs/reference/decisions/0077-jr2-requires-landlock-on-every-sandbox-node.md +79 -0
- package/docs/reference/decisions/0078-a-caller-comes-from-one-authenticator-seam-and-acts-within-its-scopes.md +285 -0
- package/docs/reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md +198 -0
- package/docs/reference/decisions.md +9 -4
- package/docs/reference/glossary.md +138 -45
- package/docs/reference/hooks.md +1077 -0
- package/docs/reference/http-api.md +988 -123
- package/docs/reference/machines.md +0 -1
- package/docs/why.md +7 -5
- package/package.json +10 -3
- package/src/actor.ts +194 -42
- package/src/agent.ts +56 -6
- package/src/ambient.ts +3 -5
- package/src/assembly-checks.ts +530 -4
- package/src/callers.ts +245 -0
- package/src/chart.ts +238 -26
- package/src/charts.ts +9 -2
- package/src/config.ts +566 -7
- package/src/conversations.ts +453 -0
- package/src/custodian.lua +17 -5
- package/src/custodian.ts +257 -77
- package/src/customize.ts +154 -7
- package/src/dispatch-effect.ts +142 -26
- package/src/durability.ts +2 -0
- package/src/duration.ts +20 -0
- package/src/effect-checks.ts +52 -4
- package/src/effect.ts +401 -132
- package/src/effector-wire.ts +278 -30
- package/src/fail.ts +17 -4
- package/src/gate-brand.ts +16 -0
- package/src/gate.ts +634 -48
- package/src/harness-client.ts +84 -35
- package/src/harness-env.ts +33 -0
- package/src/held-secrets.ts +197 -39
- package/src/holds.ts +5 -2
- package/src/hook-kinds/discord.ts +92 -0
- package/src/hook-kinds/door.ts +107 -0
- package/src/hook-kinds/github.ts +89 -0
- package/src/hook-kinds/gitlab.ts +72 -0
- package/src/hook-kinds/hook.ts +7 -0
- package/src/hook-kinds/index.ts +465 -0
- package/src/hook-kinds/slack.ts +151 -0
- package/src/hook-kinds/webhook.ts +113 -0
- package/src/hooks.ts +506 -0
- package/src/http-effect.ts +164 -70
- package/src/http.ts +933 -99
- package/src/index.ts +30 -5
- package/src/instance.ts +196 -26
- package/src/internal.ts +23 -3
- package/src/kube-client.ts +6 -3
- package/src/machine-doc.ts +29 -5
- package/src/mcp-effect.ts +424 -0
- package/src/model-keys.ts +24 -0
- package/src/mount-digest.ts +41 -7
- package/src/names.ts +82 -7
- package/src/notices.ts +576 -59
- package/src/open-body.ts +110 -30
- package/src/open-effect.ts +17 -4
- package/src/open.ts +40 -7
- package/src/parts.ts +125 -35
- package/src/party.ts +35 -1
- package/src/pick.ts +43 -9
- package/src/pool.ts +1172 -172
- package/src/registration.ts +249 -17
- package/src/repo-identity.ts +7 -3
- package/src/repos.ts +20 -2
- package/src/run-host.ts +1614 -190
- package/src/run-list.ts +191 -0
- package/src/sandbox-effect.ts +217 -66
- package/src/sandbox-kube.ts +116 -21
- package/src/secret-placement.ts +248 -35
- package/src/server.ts +53 -4
- package/src/setup.ts +158 -38
- package/src/snapshot-store.ts +565 -48
- package/src/steer.ts +111 -0
- package/src/stub-harness.ts +6 -2
- package/src/tokens.ts +362 -46
- package/src/vocabulary.ts +23 -4
- package/src/wire.ts +27 -10
- package/src/workspace.ts +314 -259
- package/src/worktree.ts +249 -0
- package/docs/reference/authoring-surface/type-noslotspec.md +0 -26
- package/docs/reference/authoring-surface/type-workspacespec.md +0 -37
- package/docs/reference/decisions/0032-the-console-unlocks-with-the-instance-token.md +0 -61
- package/docs/reference/decisions/0065-a-persist-survives-a-crash-and-history-is-a-week.md +0 -65
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Call an MCP tool from a Machine
|
|
3
|
+
description: Call a tool of an MCP server as an Effect, after the approval of a person, and route on its output.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Call an MCP tool from a Machine
|
|
7
|
+
|
|
8
|
+
Call a tool of an MCP server as an [Effect](../reference/glossary.md#effect), after a person approves the call, and
|
|
9
|
+
route the [Machine](../reference/glossary.md#machine) on its output.
|
|
10
|
+
|
|
11
|
+
An [Agent](../reference/glossary.md#agent) that holds [MCP tools](../reference/glossary.md#mcp-tools) calls them with no
|
|
12
|
+
approval, within its allowlist. When a call needs the approval of a person, the Machine makes the call: the Agent picks,
|
|
13
|
+
a [Gate](../reference/glossary.md#gate) waits for the approval, and an [`mcp`](../reference/authoring-surface/mcp.md)
|
|
14
|
+
Effect calls the tool. The output of the Effect is the answer to the pick. Thus the approval is in the record of the
|
|
15
|
+
run, it stays across a restart, and the Machine routes on it.
|
|
16
|
+
|
|
17
|
+
In this [Workflow](../reference/glossary.md#workflow), the Machine first reads a resource and gets a prompt from the
|
|
18
|
+
server. Then a writer finds the channel with `find_channel`, and asks to post the announcement with `post_message`. A
|
|
19
|
+
person approves the message, and the Machine posts it. Save the file as `workflows/announce.ts` in your
|
|
20
|
+
[Instance](../reference/glossary.md#instance):
|
|
21
|
+
|
|
22
|
+
**Anthropic**
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { assign } from "xstate";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import {
|
|
28
|
+
agent,
|
|
29
|
+
answer,
|
|
30
|
+
defineEvent,
|
|
31
|
+
fail,
|
|
32
|
+
fromPick,
|
|
33
|
+
jr2Setup,
|
|
34
|
+
mcp,
|
|
35
|
+
type McpCallOutput,
|
|
36
|
+
type McpReadOutput,
|
|
37
|
+
} from "@jr2/orchestrator";
|
|
38
|
+
|
|
39
|
+
// The writer's picks. Each one is a tool on its Menu, and the Machine makes the MCP call.
|
|
40
|
+
const findChannel = defineEvent({
|
|
41
|
+
name: "find_channel",
|
|
42
|
+
description: "Find a channel by a part of its name. The answer lists the channels that match.",
|
|
43
|
+
input: z.object({ query: z.string().describe("A part of the channel's name.") }),
|
|
44
|
+
});
|
|
45
|
+
const postMessage = defineEvent({
|
|
46
|
+
name: "post_message",
|
|
47
|
+
description: "Post one message to a channel. A person approves it first. The answer tells if it went out.",
|
|
48
|
+
input: z.object({ channel: z.string().describe("The channel id."), text: z.string().describe("The message.") }),
|
|
49
|
+
});
|
|
50
|
+
const finished = defineEvent({
|
|
51
|
+
name: "finished",
|
|
52
|
+
description: "Call this after the announcement went out.",
|
|
53
|
+
input: z.object({}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// The answers of the person at the Gate. The writer cannot send them.
|
|
57
|
+
const approve = defineEvent({ name: "approve", description: "Post this message.", input: z.object({}) });
|
|
58
|
+
const reject = defineEvent({
|
|
59
|
+
name: "reject",
|
|
60
|
+
description: "Do not post this message. Say why.",
|
|
61
|
+
input: z.object({ reason: z.string().min(1) }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const door = z.object({ version: z.string().describe("The version to announce, such as 1.4.0.") });
|
|
65
|
+
|
|
66
|
+
type Input = z.infer<typeof door>;
|
|
67
|
+
type Ctx = Input & { rules?: string; brief?: string; channel?: string; text?: string; posted: boolean };
|
|
68
|
+
|
|
69
|
+
// What the writer reads when the server did not answer as a tool does. `reason` says why.
|
|
70
|
+
const notReturned = (output: McpCallOutput): string =>
|
|
71
|
+
output.outcome === "unknown"
|
|
72
|
+
? `jr2 cannot tell if the call acted: ${output.reason}. Do not call it again.`
|
|
73
|
+
: `The call did not run: ${"reason" in output ? output.reason : output.outcome}`;
|
|
74
|
+
|
|
75
|
+
// The text of a read or a prompt, or undefined when the server did not give it.
|
|
76
|
+
const textOf = (output: McpReadOutput): string | undefined => (output.outcome === "returned" ? output.text : undefined);
|
|
77
|
+
|
|
78
|
+
export const machine = jr2Setup({
|
|
79
|
+
types: {} as { context: Ctx; input: Input },
|
|
80
|
+
events: [findChannel, postMessage, finished, approve, reject],
|
|
81
|
+
actors: {
|
|
82
|
+
writer: agent({
|
|
83
|
+
model: "anthropic/claude-haiku-5-5",
|
|
84
|
+
instructions: "You write short release announcements for the team chat.",
|
|
85
|
+
workspace: "none",
|
|
86
|
+
}),
|
|
87
|
+
// A resource and a prompt of the server: the Machine reads them before the writer starts.
|
|
88
|
+
rules: mcp.read("slack-acme", "guide://announcements", { idempotent: true }),
|
|
89
|
+
brief: mcp.prompt("slack-acme", "announcement", { input: z.object({ version: z.string() }), idempotent: true }),
|
|
90
|
+
// A search changes nothing, so it is safe to send two times.
|
|
91
|
+
search: mcp.call("slack-acme", "find_channel", { input: z.object({ query: z.string() }), idempotent: true }),
|
|
92
|
+
// A post changes the channel, so jr2 sends it at most one time.
|
|
93
|
+
post: mcp.call("slack-acme", "post_message", { input: z.object({ channel: z.string(), text: z.string() }) }),
|
|
94
|
+
fail: fail(),
|
|
95
|
+
},
|
|
96
|
+
}).createMachine({
|
|
97
|
+
id: "announce",
|
|
98
|
+
input: door,
|
|
99
|
+
context: ({ input }) => ({ version: input.version, posted: false }),
|
|
100
|
+
on: { "agent.fault": ".failed" },
|
|
101
|
+
initial: "reading",
|
|
102
|
+
states: {
|
|
103
|
+
reading: {
|
|
104
|
+
invoke: {
|
|
105
|
+
src: "rules",
|
|
106
|
+
onDone: [
|
|
107
|
+
{
|
|
108
|
+
guard: ({ event }) => textOf(event.output) !== undefined,
|
|
109
|
+
target: "briefing",
|
|
110
|
+
actions: assign({ rules: ({ event }) => textOf(event.output) }),
|
|
111
|
+
},
|
|
112
|
+
{ target: "failed" },
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
briefing: {
|
|
117
|
+
invoke: {
|
|
118
|
+
src: "brief",
|
|
119
|
+
input: ({ context }) => ({ version: context.version }),
|
|
120
|
+
onDone: [
|
|
121
|
+
{
|
|
122
|
+
guard: ({ event }) => textOf(event.output) !== undefined,
|
|
123
|
+
target: "writing",
|
|
124
|
+
actions: assign({ brief: ({ event }) => textOf(event.output) }),
|
|
125
|
+
},
|
|
126
|
+
{ target: "failed" },
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
// The writer's state. Its Turn lasts until `finished` leaves it.
|
|
131
|
+
writing: {
|
|
132
|
+
invoke: {
|
|
133
|
+
src: "writer",
|
|
134
|
+
input: ({ context }) => ({ prompt: `${context.brief}\n\nThe rules of the channel:\n${context.rules}` }),
|
|
135
|
+
},
|
|
136
|
+
on: { finished: { guard: ({ context }) => context.posted, target: "done" } },
|
|
137
|
+
initial: "ready",
|
|
138
|
+
states: {
|
|
139
|
+
ready: {
|
|
140
|
+
on: {
|
|
141
|
+
find_channel: "searching",
|
|
142
|
+
post_message: {
|
|
143
|
+
target: "approving",
|
|
144
|
+
actions: assign({ channel: ({ event }) => event.channel, text: ({ event }) => event.text }),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
// The pick enters the Effect's state, so fromPick gives its `query` to the tool's arguments.
|
|
149
|
+
searching: {
|
|
150
|
+
invoke: {
|
|
151
|
+
src: "search",
|
|
152
|
+
input: fromPick(findChannel),
|
|
153
|
+
onDone: {
|
|
154
|
+
target: "ready",
|
|
155
|
+
// The writer reads this text as the answer to its find_channel pick.
|
|
156
|
+
actions: answer(({ event }) =>
|
|
157
|
+
event.output.outcome === "returned" ? event.output.text : notReturned(event.output),
|
|
158
|
+
),
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
// A person approves the message. While the Gate is open, the post_message pick is outstanding.
|
|
163
|
+
approving: {
|
|
164
|
+
invoke: {
|
|
165
|
+
src: "gate",
|
|
166
|
+
input: ({ context }) => ({ meta: { channel: context.channel, text: context.text } }),
|
|
167
|
+
},
|
|
168
|
+
on: {
|
|
169
|
+
approve: "posting",
|
|
170
|
+
reject: { target: "ready", actions: answer(({ event }) => `The editor rejected it: ${event.reason}`) },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
// The Gate came between the pick and the Effect, so the input comes from the context.
|
|
174
|
+
posting: {
|
|
175
|
+
invoke: {
|
|
176
|
+
src: "post",
|
|
177
|
+
input: ({ context }) => ({ channel: context.channel!, text: context.text! }),
|
|
178
|
+
onDone: [
|
|
179
|
+
{
|
|
180
|
+
guard: ({ event }) => event.output.outcome === "returned" && !event.output.isError,
|
|
181
|
+
target: "ready",
|
|
182
|
+
actions: [assign({ posted: true }), answer(() => "The message went out.")],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
target: "ready",
|
|
186
|
+
actions: answer(({ event }) =>
|
|
187
|
+
event.output.outcome === "returned"
|
|
188
|
+
? `The tool failed: ${event.output.text}`
|
|
189
|
+
: notReturned(event.output),
|
|
190
|
+
),
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
done: { type: "final" },
|
|
198
|
+
// Not a final state: the run ends `failed`, with the reason.
|
|
199
|
+
failed: { invoke: { src: "fail", input: { reason: "the announcement did not go out" } } },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Local**
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { assign } from "xstate";
|
|
208
|
+
import { z } from "zod";
|
|
209
|
+
import {
|
|
210
|
+
agent,
|
|
211
|
+
answer,
|
|
212
|
+
defineEvent,
|
|
213
|
+
fail,
|
|
214
|
+
fromPick,
|
|
215
|
+
jr2Setup,
|
|
216
|
+
mcp,
|
|
217
|
+
type McpCallOutput,
|
|
218
|
+
type McpReadOutput,
|
|
219
|
+
} from "@jr2/orchestrator";
|
|
220
|
+
|
|
221
|
+
// The writer's picks. Each one is a tool on its Menu, and the Machine makes the MCP call.
|
|
222
|
+
const findChannel = defineEvent({
|
|
223
|
+
name: "find_channel",
|
|
224
|
+
description: "Find a channel by a part of its name. The answer lists the channels that match.",
|
|
225
|
+
input: z.object({ query: z.string().describe("A part of the channel's name.") }),
|
|
226
|
+
});
|
|
227
|
+
const postMessage = defineEvent({
|
|
228
|
+
name: "post_message",
|
|
229
|
+
description: "Post one message to a channel. A person approves it first. The answer tells if it went out.",
|
|
230
|
+
input: z.object({ channel: z.string().describe("The channel id."), text: z.string().describe("The message.") }),
|
|
231
|
+
});
|
|
232
|
+
const finished = defineEvent({
|
|
233
|
+
name: "finished",
|
|
234
|
+
description: "Call this after the announcement went out.",
|
|
235
|
+
input: z.object({}),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// The answers of the person at the Gate. The writer cannot send them.
|
|
239
|
+
const approve = defineEvent({ name: "approve", description: "Post this message.", input: z.object({}) });
|
|
240
|
+
const reject = defineEvent({
|
|
241
|
+
name: "reject",
|
|
242
|
+
description: "Do not post this message. Say why.",
|
|
243
|
+
input: z.object({ reason: z.string().min(1) }),
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const door = z.object({ version: z.string().describe("The version to announce, such as 1.4.0.") });
|
|
247
|
+
|
|
248
|
+
type Input = z.infer<typeof door>;
|
|
249
|
+
type Ctx = Input & { rules?: string; brief?: string; channel?: string; text?: string; posted: boolean };
|
|
250
|
+
|
|
251
|
+
// What the writer reads when the server did not answer as a tool does. `reason` says why.
|
|
252
|
+
const notReturned = (output: McpCallOutput): string =>
|
|
253
|
+
output.outcome === "unknown"
|
|
254
|
+
? `jr2 cannot tell if the call acted: ${output.reason}. Do not call it again.`
|
|
255
|
+
: `The call did not run: ${"reason" in output ? output.reason : output.outcome}`;
|
|
256
|
+
|
|
257
|
+
// The text of a read or a prompt, or undefined when the server did not give it.
|
|
258
|
+
const textOf = (output: McpReadOutput): string | undefined => (output.outcome === "returned" ? output.text : undefined);
|
|
259
|
+
|
|
260
|
+
export const machine = jr2Setup({
|
|
261
|
+
types: {} as { context: Ctx; input: Input },
|
|
262
|
+
events: [findChannel, postMessage, finished, approve, reject],
|
|
263
|
+
actors: {
|
|
264
|
+
writer: agent({
|
|
265
|
+
model: "local/qwen3.6-35b-a3b",
|
|
266
|
+
instructions: "You write short release announcements for the team chat.",
|
|
267
|
+
workspace: "none",
|
|
268
|
+
}),
|
|
269
|
+
// A resource and a prompt of the server: the Machine reads them before the writer starts.
|
|
270
|
+
rules: mcp.read("slack-acme", "guide://announcements", { idempotent: true }),
|
|
271
|
+
brief: mcp.prompt("slack-acme", "announcement", { input: z.object({ version: z.string() }), idempotent: true }),
|
|
272
|
+
// A search changes nothing, so it is safe to send two times.
|
|
273
|
+
search: mcp.call("slack-acme", "find_channel", { input: z.object({ query: z.string() }), idempotent: true }),
|
|
274
|
+
// A post changes the channel, so jr2 sends it at most one time.
|
|
275
|
+
post: mcp.call("slack-acme", "post_message", { input: z.object({ channel: z.string(), text: z.string() }) }),
|
|
276
|
+
fail: fail(),
|
|
277
|
+
},
|
|
278
|
+
}).createMachine({
|
|
279
|
+
id: "announce",
|
|
280
|
+
input: door,
|
|
281
|
+
context: ({ input }) => ({ version: input.version, posted: false }),
|
|
282
|
+
on: { "agent.fault": ".failed" },
|
|
283
|
+
initial: "reading",
|
|
284
|
+
states: {
|
|
285
|
+
reading: {
|
|
286
|
+
invoke: {
|
|
287
|
+
src: "rules",
|
|
288
|
+
onDone: [
|
|
289
|
+
{
|
|
290
|
+
guard: ({ event }) => textOf(event.output) !== undefined,
|
|
291
|
+
target: "briefing",
|
|
292
|
+
actions: assign({ rules: ({ event }) => textOf(event.output) }),
|
|
293
|
+
},
|
|
294
|
+
{ target: "failed" },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
briefing: {
|
|
299
|
+
invoke: {
|
|
300
|
+
src: "brief",
|
|
301
|
+
input: ({ context }) => ({ version: context.version }),
|
|
302
|
+
onDone: [
|
|
303
|
+
{
|
|
304
|
+
guard: ({ event }) => textOf(event.output) !== undefined,
|
|
305
|
+
target: "writing",
|
|
306
|
+
actions: assign({ brief: ({ event }) => textOf(event.output) }),
|
|
307
|
+
},
|
|
308
|
+
{ target: "failed" },
|
|
309
|
+
],
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
// The writer's state. Its Turn lasts until `finished` leaves it.
|
|
313
|
+
writing: {
|
|
314
|
+
invoke: {
|
|
315
|
+
src: "writer",
|
|
316
|
+
input: ({ context }) => ({ prompt: `${context.brief}\n\nThe rules of the channel:\n${context.rules}` }),
|
|
317
|
+
},
|
|
318
|
+
on: { finished: { guard: ({ context }) => context.posted, target: "done" } },
|
|
319
|
+
initial: "ready",
|
|
320
|
+
states: {
|
|
321
|
+
ready: {
|
|
322
|
+
on: {
|
|
323
|
+
find_channel: "searching",
|
|
324
|
+
post_message: {
|
|
325
|
+
target: "approving",
|
|
326
|
+
actions: assign({ channel: ({ event }) => event.channel, text: ({ event }) => event.text }),
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
// The pick enters the Effect's state, so fromPick gives its `query` to the tool's arguments.
|
|
331
|
+
searching: {
|
|
332
|
+
invoke: {
|
|
333
|
+
src: "search",
|
|
334
|
+
input: fromPick(findChannel),
|
|
335
|
+
onDone: {
|
|
336
|
+
target: "ready",
|
|
337
|
+
// The writer reads this text as the answer to its find_channel pick.
|
|
338
|
+
actions: answer(({ event }) =>
|
|
339
|
+
event.output.outcome === "returned" ? event.output.text : notReturned(event.output),
|
|
340
|
+
),
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
// A person approves the message. While the Gate is open, the post_message pick is outstanding.
|
|
345
|
+
approving: {
|
|
346
|
+
invoke: {
|
|
347
|
+
src: "gate",
|
|
348
|
+
input: ({ context }) => ({ meta: { channel: context.channel, text: context.text } }),
|
|
349
|
+
},
|
|
350
|
+
on: {
|
|
351
|
+
approve: "posting",
|
|
352
|
+
reject: { target: "ready", actions: answer(({ event }) => `The editor rejected it: ${event.reason}`) },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
// The Gate came between the pick and the Effect, so the input comes from the context.
|
|
356
|
+
posting: {
|
|
357
|
+
invoke: {
|
|
358
|
+
src: "post",
|
|
359
|
+
input: ({ context }) => ({ channel: context.channel!, text: context.text! }),
|
|
360
|
+
onDone: [
|
|
361
|
+
{
|
|
362
|
+
guard: ({ event }) => event.output.outcome === "returned" && !event.output.isError,
|
|
363
|
+
target: "ready",
|
|
364
|
+
actions: [assign({ posted: true }), answer(() => "The message went out.")],
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
target: "ready",
|
|
368
|
+
actions: answer(({ event }) =>
|
|
369
|
+
event.output.outcome === "returned"
|
|
370
|
+
? `The tool failed: ${event.output.text}`
|
|
371
|
+
: notReturned(event.output),
|
|
372
|
+
),
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
done: { type: "final" },
|
|
380
|
+
// Not a final state: the run ends `failed`, with the reason.
|
|
381
|
+
failed: { invoke: { src: "fail", input: { reason: "the announcement did not go out" } } },
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The writer uses the model of the tab. The `mcp` Effects name the server `slack-acme`. Declare it in the
|
|
387
|
+
[`mcp`](../reference/config.md#mcp) block of `jr2.config.ts`, beside the fields and the entries that are there:
|
|
388
|
+
|
|
389
|
+
```ts
|
|
390
|
+
import { defineConfig } from "@jr2/orchestrator";
|
|
391
|
+
|
|
392
|
+
export default defineConfig({
|
|
393
|
+
// The two tokens. The values come from .env.
|
|
394
|
+
secrets: [
|
|
395
|
+
{ name: "SLACK_ACME_TOKEN", value: process.env.SLACK_ACME_TOKEN },
|
|
396
|
+
{ name: "SLACK_PARTNER_TOKEN", value: process.env.SLACK_PARTNER_TOKEN },
|
|
397
|
+
],
|
|
398
|
+
// Two servers of one service, each with its own token. The names are yours.
|
|
399
|
+
mcp: {
|
|
400
|
+
"slack-acme": { url: "https://mcp.slack.example/mcp", secret: "SLACK_ACME_TOKEN" },
|
|
401
|
+
"slack-partner": { url: "https://mcp.slack.example/mcp", secret: "SLACK_PARTNER_TOKEN", timeout: "90s" },
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
`https://mcp.slack.example/mcp` is an example. Write the URL of the MCP endpoint of your service, and put the token in
|
|
407
|
+
`.env`. [Give an Agent MCP tools](give-an-agent-mcp-tools.md) tells what each field of the block does.
|
|
408
|
+
`guide://announcements` and `announcement` are examples too: write a resource and a prompt that your server has.
|
|
409
|
+
|
|
410
|
+
## Call a tool
|
|
411
|
+
|
|
412
|
+
`mcp.call(server, tool, options)` calls one tool. `server` is a name of the `mcp` block, and `tool` is the name of the
|
|
413
|
+
tool on the server. Put the Effect under `actors` in [`jr2Setup`](../reference/authoring-surface/jr2setup.md), and
|
|
414
|
+
invoke it by its key from a state. The options:
|
|
415
|
+
|
|
416
|
+
- `input`: the zod schema of the input of the Effect. jr2 parses the input with it before the call. The parsed input is
|
|
417
|
+
the arguments of the tool. With no schema, the input of the invoke is the arguments as it is.
|
|
418
|
+
- `timeout`: how long jr2 waits for the answer, such as `"30s"`. The default is the `timeout` of the server in
|
|
419
|
+
`jr2.config.ts`, 60 seconds when it sets none. After it, the outcome is `unknown`.
|
|
420
|
+
- `maxBody`: how many bytes of `text`, and of `structured` as JSON, the output keeps. The default is 65536.
|
|
421
|
+
- `idempotent`: `true` when it is safe to send the call two times, such as a search. The default is `false`. jr2 does
|
|
422
|
+
not read the `idempotentHint` of the tool, because the server is not the author of the Machine.
|
|
423
|
+
|
|
424
|
+
An `mcp` Effect runs in the [Instance Effector](../reference/glossary.md#instance-effector). The `secret` of the server
|
|
425
|
+
is its [Effect secret](../reference/glossary.md#effect-secret): `jr2 up` puts it in the Instance Effector, and the
|
|
426
|
+
Effector sends it as `Authorization: Bearer <value>`. The Agent and the Orchestrator never read the value. You can
|
|
427
|
+
invoke an `mcp` Effect in a Machine with no [Workspace](../reference/glossary.md#workspace). An `mcp` Effect calls a
|
|
428
|
+
remote server only.
|
|
429
|
+
|
|
430
|
+
## Route on the output
|
|
431
|
+
|
|
432
|
+
Each outcome is output for `onDone`. Route on `outcome`
|
|
433
|
+
([`McpCallOutput`](../reference/authoring-surface/type-mcpcalloutput.md)):
|
|
434
|
+
|
|
435
|
+
- `returned`, with `isError`, `text`, and `structured` when the result has `structuredContent`. `isError` is `true` when
|
|
436
|
+
the tool ran and says that it failed. `text` joins the text of the result. A part that is not text is one line that
|
|
437
|
+
names its kind. Past `maxBody`, jr2 cuts `text`, drops `structured`, and sets `truncated`.
|
|
438
|
+
- `refused`: the call did not run. The server answered an HTTP 3xx or 4xx status, such as 401 for a wrong token. Or it
|
|
439
|
+
refused the request as one that it cannot run, such as an unknown tool or bad arguments.
|
|
440
|
+
- `unreachable`: jr2 sent nothing of the call. The server did not answer, or it does not speak MCP, or it speaks a
|
|
441
|
+
revision of MCP that jr2 does not speak.
|
|
442
|
+
- `input-required`: the server asked for input, which jr2 does not give. The tool can have started. jr2 does not send
|
|
443
|
+
the call again.
|
|
444
|
+
- `unknown`: jr2 sent the call, and no answer came, or the server answered with an error after it got the call. The tool
|
|
445
|
+
can have acted. jr2 does not send it again, unless it is `idempotent`.
|
|
446
|
+
|
|
447
|
+
Each outcome other than `returned` has a `reason` in words. The union is open: give the rest a last arm with no guard.
|
|
448
|
+
jr2 changes each value of an Effect secret in the output to `[secret:<name>]`.
|
|
449
|
+
|
|
450
|
+
In the example, the writer reads the result of each call as the answer to its pick. After `unknown`, it reads that jr2
|
|
451
|
+
cannot tell if the call acted. The Machine does not send the call again by itself: the writer decides if it posts again.
|
|
452
|
+
|
|
453
|
+
## Approve the call first
|
|
454
|
+
|
|
455
|
+
The `post_message` pick goes to `approving`, a child state of the writer's state that invokes a Gate. The writer waits
|
|
456
|
+
for the answer to its pick while the Gate is open. A person answers the Gate with `approve` or `reject`:
|
|
457
|
+
|
|
458
|
+
- `approve` goes to `posting`, which invokes the `mcp` Effect. Its output is the answer to the pick.
|
|
459
|
+
- `reject` goes back to `ready`, and the writer reads the reason as the answer.
|
|
460
|
+
|
|
461
|
+
The pick enters `approving`, and `approve` enters `posting`. Thus the input of `post` comes from the context, where the
|
|
462
|
+
pick put the channel and the text. [Answer a Gate](answer-a-gate.md) tells how a person answers.
|
|
463
|
+
|
|
464
|
+
## Give the input from a pick
|
|
465
|
+
|
|
466
|
+
When a pick enters the state of the Effect, [`fromPick(def)`](../reference/authoring-surface/frompick.md) gives each
|
|
467
|
+
param of the pick to the key of the same name. In `searching`, the `query` of `find_channel` goes to the `query` of the
|
|
468
|
+
tool's arguments. `fromPick(def, fill)` adds the keys that `fill` gives, so that the Machine sets a key that the Agent
|
|
469
|
+
cannot choose. TypeScript checks the keys against the `input` of the Effect. A Gate between the pick and the Effect
|
|
470
|
+
gives a different event, so use the context there, as `posting` does.
|
|
471
|
+
[Run a command or call a URL with an Effect](run-a-command-or-call-a-url.md#give-the-input-from-a-pick) gives the rules
|
|
472
|
+
of `fromPick`.
|
|
473
|
+
|
|
474
|
+
## Read a resource and get a prompt
|
|
475
|
+
|
|
476
|
+
- `mcp.read(server, uri, options)` reads one resource. `uri` is the URI of the resource on the server. The output
|
|
477
|
+
([`McpReadOutput`](../reference/authoring-surface/type-mcpreadoutput.md)) is `returned` with `text`, the text of the
|
|
478
|
+
contents, and `structured`, the list of the contents. A binary part is given only as its type and its size.
|
|
479
|
+
- `mcp.prompt(server, name, options)` gets one prompt. `name` is the name of the prompt. The `input` schema is the
|
|
480
|
+
arguments of the prompt, and each field is a string. The output is `returned` with `text`, the text of the messages,
|
|
481
|
+
and `structured`, the list of the messages.
|
|
482
|
+
|
|
483
|
+
Their other outcomes are those of a call, with no `isError`. `uri` and `name` are always yours. The options are the
|
|
484
|
+
options of a call: `input` for a prompt, `timeout`, `maxBody` and `idempotent`. A read and most prompts change nothing,
|
|
485
|
+
so the example marks them `idempotent`.
|
|
486
|
+
|
|
487
|
+
## Bind the server or the tool of a packaged Machine
|
|
488
|
+
|
|
489
|
+
A Machine from a package can leave the server and the tool of an `mcp` Effect [Open](../reference/glossary.md#open):
|
|
490
|
+
`mcp.call(open, open, { input })`. Bind them with `effects`, by the slot of the Effect, on the line that registers the
|
|
491
|
+
Machine, such as `customize(machine, { effects: { post: { server: "slack-acme", tool: "post_message" } } })`. Bind only
|
|
492
|
+
the part that is Open: `{ server }` or `{ tool }`. The `server` of `mcp.read` and `mcp.prompt` can be Open too.
|
|
493
|
+
[Bind a shipped Machine's Open parts](bind-open-parts.md#bind-the-open-parts-of-an-effect) tells more.
|
|
494
|
+
|
|
495
|
+
## What jr2 check shows
|
|
496
|
+
|
|
497
|
+
Run [`jr2 check`](../reference/cli.md#check). It reads each Effect without running it, and it shows what the Effect
|
|
498
|
+
runs, and where. [`jr2 up`](../reference/cli.md#up) runs the same check before it converges:
|
|
499
|
+
|
|
500
|
+
```sh
|
|
501
|
+
jr2 check
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```text
|
|
505
|
+
jr2 check — instance "my-instance" (no cluster)
|
|
506
|
+
typecheck: tsc --noEmit (the instance's own compiler)
|
|
507
|
+
effects: workflow "announce" — what it runs outside itself (ADR-0067)
|
|
508
|
+
rules: mcp read slack-acme — the server's timeout, in the Instance Effector, idempotent
|
|
509
|
+
brief: mcp prompt slack-acme — the server's timeout, in the Instance Effector, idempotent
|
|
510
|
+
search: mcp call slack-acme/find_channel — the server's timeout, in the Instance Effector, idempotent
|
|
511
|
+
post: mcp call slack-acme/post_message — the server's timeout, in the Instance Effector
|
|
512
|
+
failures: workflow "announce" — the states that fail the run (ADR-0073)
|
|
513
|
+
announce.failed: invokes fail
|
|
514
|
+
seats: workflow "announce" — who may send what, per state (ADR-0015)
|
|
515
|
+
announce.writing agent:writer: [find_channel, finished, post_message]
|
|
516
|
+
announce.writing.approving gate: [approve, reject]
|
|
517
|
+
agents: writer (carried by 2 workflow machine(s))
|
|
518
|
+
check: passed — 2 workflow(s): announce, ping; `jr2 up` stages the Assembly and converges the cluster after the same check
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
`seats` shows that only the Gate takes `approve` and `reject`. Thus the writer cannot approve its own message.
|
|
522
|
+
|
|
523
|
+
## Rules
|
|
524
|
+
|
|
525
|
+
When you call `mcp.call()`, `mcp.read()` or `mcp.prompt()`, it refuses:
|
|
526
|
+
|
|
527
|
+
- a server name that does not have the form of a name of the `mcp` block, or that is `jr2`:
|
|
528
|
+
``mcp.<op>(server): "<server>" is not a server name — a name of the mcp block of jr2.config.ts, lowercase letters, digits and -, not "jr2"; or `open`, which the composer binds (ADR-0079)``
|
|
529
|
+
- an empty tool, URI or prompt name. `<what>` is `tool`, `uri` or `prompt name`:
|
|
530
|
+
`mcp.<op>(): the <what> is a non-empty string`
|
|
531
|
+
- `open` for a URI or a prompt name:
|
|
532
|
+
`mcp.<op>(): its <what> is the author's, never Open — only a call's tool is (ADR-0079)`
|
|
533
|
+
- a timeout that is not a positive duration: `mcp: timeout "<timeout>" is not a positive duration (like "30s", "2m")`
|
|
534
|
+
- a `maxBody` that is not a positive whole number: `mcp({ maxBody }): a positive number of bytes`
|
|
535
|
+
- a field of a prompt's `input` that is not a string:
|
|
536
|
+
`mcp.prompt("<name>"): the input field "<key>" is not a string schema, and a prompt's arguments are strings (MCP's prompts/get)`
|
|
537
|
+
|
|
538
|
+
`jr2 check` and `jr2 up` refuse:
|
|
539
|
+
|
|
540
|
+
- a server that the `mcp` block does not declare:
|
|
541
|
+
`refusing: workflow "<workflow>": the mcp Effect "<slot>" names the MCP server "<server>", which the mcp block declares "<names>" — declare it in jr2.config.ts: mcp: { "<server>": { url: "https://<host>/mcp", secret: "<SECRET>" } }, or name a declared remote server (ADR-0079)`
|
|
542
|
+
- a stdio server:
|
|
543
|
+
`refusing: workflow "<workflow>": the mcp Effect "<slot>" names the stdio MCP server "<server>" — an mcp Effect runs in the Instance Effector, which starts no program, so it calls a remote server only: declare one for it in jr2.config.ts, mcp: { "<server>": { url: "https://<host>/mcp", secret: "<SECRET>" } } (ADR-0079)`
|
|
544
|
+
- an Open part that nobody bound. `<parts>` is `server`, `tool`, or `server and tool`:
|
|
545
|
+
`refusing: workflow "<workflow>" leaves the <parts> of Effect "<slot>" open — bind it where the Machine is registered: export const machine = customize(<import>, { effects: { <slot>: { server: "<name>", tool: "<tool>" } } }) (ADR-0054)`.
|
|
546
|
+
The binding holds only the parts that are Open.
|
|
547
|
+
|
|
548
|
+
`customize()` refuses a binding that has no key, or a key that is not an Open part of the Effect. `<parts>` is the
|
|
549
|
+
Effect's Open parts: `customize(): the mcp Effect "<slot>" leaves its <parts> Open — bind it with <binding> (ADR-0054)`.
|
|
550
|
+
A binding that leaves out one Open part leaves that part Open, and `jr2 up` refuses it as above.
|
|
551
|
+
|
|
552
|
+
When an `mcp` Effect starts, it refuses an input that its `input` schema does not accept. The Effect takes its `onError`
|
|
553
|
+
route, and each problem is `<path>: <message>`, with `; ` between them:
|
|
554
|
+
`mcp "<slot>": its input does not match the definition's input schema — <path>: <message>`. With no schema, it refuses
|
|
555
|
+
an input that is not an object: `mcp "<slot>": its input is the request's arguments, an object`. A prompt refuses an
|
|
556
|
+
argument that is not a string: `mcp.prompt: the argument "<key>" is <type>, and a prompt's arguments are strings`.
|
|
557
|
+
|
|
558
|
+
## Read next
|
|
559
|
+
|
|
560
|
+
- [Effects](../concepts/effects.md): the kinds of Effect, where each runs, and at most once.
|
|
561
|
+
- [Give an Agent MCP tools](give-an-agent-mcp-tools.md): the `mcp` block, and the tools that an Agent calls itself.
|
|
562
|
+
- [Gates](../concepts/gates.md) and [Answer a Gate](answer-a-gate.md): the approval.
|
|
563
|
+
- [The Menu](../concepts/the-menu.md): how a pick waits for its answer.
|
|
564
|
+
- Reference: [`mcp`](../reference/authoring-surface/mcp.md),
|
|
565
|
+
[`McpOptions`](../reference/authoring-surface/type-mcpoptions.md),
|
|
566
|
+
[`McpCallOutput`](../reference/authoring-surface/type-mcpcalloutput.md),
|
|
567
|
+
[`McpReadOutput`](../reference/authoring-surface/type-mcpreadoutput.md),
|
|
568
|
+
[`fromPick`](../reference/authoring-surface/frompick.md) and [`answer`](../reference/authoring-surface/answer.md).
|
|
569
|
+
- [ADR-0079](../reference/decisions/0079-an-agent-holds-mcp-tools-a-machine-calls-them-as-an-effect-and-the-machine-is-the-approval.md):
|
|
570
|
+
the Machine is the approval.
|
|
571
|
+
- [ADR-0067](../reference/decisions/0067-an-effect-is-an-actor-slot-and-runs-at-most-once-unless-marked-idempotent.md):
|
|
572
|
+
an Effect runs at most once unless marked idempotent.
|
|
@@ -26,7 +26,7 @@ jr2 send <run id> --event CANCEL
|
|
|
26
26
|
|
|
27
27
|
```text
|
|
28
28
|
→ context <context> / namespace <namespace>
|
|
29
|
-
sent CANCEL to
|
|
29
|
+
sent CANCEL to 8280a27f-fb24-4399-86b4-05a7ae779e86
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
`jr2` prints the full run id, also when you give a prefix. Your run id is different. Then these things occur:
|
|
@@ -40,7 +40,7 @@ sent CANCEL to 0a90c659-a2f4-44ed-ae03-ca711ca30ce4
|
|
|
40
40
|
[Start a run from a run](start-a-run-from-a-run.md).
|
|
41
41
|
|
|
42
42
|
A `jr2 run` that is attached to the run prints `→ cancelled`, then the state that the run was in. Then it prints the
|
|
43
|
-
status of the run as JSON and exits with the code
|
|
43
|
+
status of the run as JSON and exits with the code 1.
|
|
44
44
|
|
|
45
45
|
## Keep the Sandboxes
|
|
46
46
|
|
|
@@ -52,7 +52,7 @@ jr2 send <run id> --event CANCEL --keep
|
|
|
52
52
|
|
|
53
53
|
```text
|
|
54
54
|
→ context <context> / namespace <namespace>
|
|
55
|
-
sent CANCEL to
|
|
55
|
+
sent CANCEL to 442d95df-0ff4-4231-b371-2940ef8ecead; its Workspaces are kept
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
The run ends as before, but the Orchestrator does not delete its Sandboxes. To open a shell in one, see
|
|
@@ -62,24 +62,26 @@ yet, so it has nothing to look inside.
|
|
|
62
62
|
|
|
63
63
|
Nothing renews the [Lease](../reference/glossary.md#lease) of a Workspace that you keep. The
|
|
64
64
|
[operator](../concepts/architecture.md#the-operator) deletes its Sandbox 30 minutes after the last renewal. To delete
|
|
65
|
-
the Sandbox before then, find its
|
|
65
|
+
the Sandbox before then, find its handle with [`jr2 workspace ls`](../reference/cli.md#workspace-ls). After the cancel,
|
|
66
|
+
its `RUN` column shows `released`:
|
|
66
67
|
|
|
67
68
|
```sh
|
|
68
|
-
|
|
69
|
+
jr2 workspace ls <run id>
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
[Look inside a Sandbox](look-inside-a-sandbox.md#find-the-sandbox)
|
|
72
|
-
to
|
|
72
|
+
[Look inside a Sandbox](look-inside-a-sandbox.md#find-the-sandbox) tells what each column shows. Then give the handle in
|
|
73
|
+
the `WORKSPACE` column to [`jr2 workspace rm`](../reference/cli.md#workspace-rm):
|
|
73
74
|
|
|
74
75
|
```sh
|
|
75
|
-
|
|
76
|
+
jr2 workspace rm <workspace>
|
|
76
77
|
```
|
|
77
78
|
|
|
78
79
|
```text
|
|
79
|
-
|
|
80
|
+
→ context <context> / namespace <namespace>
|
|
81
|
+
removed Workspace 442d95df/x:47 (ws-442d95df-x-47-11p1uzl)
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
Your Sandbox name
|
|
84
|
+
Your handle and your Sandbox name are different. `jr2 workspace rm` refuses the Sandbox of a run that is live.
|
|
83
85
|
|
|
84
86
|
## Read how the run ended
|
|
85
87
|
|
|
@@ -89,7 +91,10 @@ jr2 status <run id>
|
|
|
89
91
|
|
|
90
92
|
```text
|
|
91
93
|
→ context <context> / namespace <namespace>
|
|
92
|
-
{"runId":"
|
|
94
|
+
{"runId":"8280a27f-fb24-4399-86b4-05a7ae779e86","workflow":"task","instanceId":"88d5f67e-d6c7-4a44-bd1d-7e0e5973aaa3","startedAt":"2026-11-06T19:23:56.027Z","status":"cancelled","value":"running",…}
|
|
95
|
+
started by: token/instance on behalf of kubernetes-admin
|
|
96
|
+
cancelled by: token/instance on behalf of kubernetes-admin
|
|
97
|
+
worktree: /work/target/task (target), branch jr2/task-88d5f67e-d6c7-4a44-bd1d-7e0e5973aaa3
|
|
93
98
|
```
|
|
94
99
|
|
|
95
100
|
`jr2 status` prints the status of the run as one line of JSON. Its `status` is `cancelled`, and its `value` is the state
|
|
@@ -98,10 +103,12 @@ that the run was in when you cancelled it.
|
|
|
98
103
|
## Cancel from HTTP
|
|
99
104
|
|
|
100
105
|
A client that is not the CLI sends `CANCEL` to
|
|
101
|
-
[`POST /runs/:runId/events`](../reference/http-api.md#post-runs-runid-events), with the
|
|
102
|
-
|
|
103
|
-
[
|
|
104
|
-
|
|
106
|
+
[`POST /runs/:runId/events`](../reference/http-api.md#post-runs-runid-events), with the credential of a
|
|
107
|
+
[Caller](../reference/glossary.md#caller) that has the `cancel` scope for the Workflow of the run. The Instance token
|
|
108
|
+
has it. [Give a Caller a scope](give-a-caller-a-scope.md) makes a token with less. The path takes the full run id, not a
|
|
109
|
+
prefix. To get the Instance token, see [Open the Console](open-the-console.md#get-the-instance-token). To keep the
|
|
110
|
+
Sandboxes, add `"keep": true` to the body. This example reaches the Orchestrator through the port forward in
|
|
111
|
+
[Open the Console](open-the-console.md):
|
|
105
112
|
|
|
106
113
|
```sh
|
|
107
114
|
curl -X POST http://localhost:4000/runs/<run id>/events \
|
|
@@ -134,7 +141,10 @@ curl -X POST http://localhost:4000/runs/<run id>/events \
|
|
|
134
141
|
with the status 400: `unknown event type "<type>" (accepts: CANCEL)`
|
|
135
142
|
- It refuses a `keep` that is not `true` or `false` with the status 400: `"keep" must be a boolean`
|
|
136
143
|
- It refuses a request with no valid token with the status 401: `unauthorized`. It refuses a Sandbox token with the
|
|
137
|
-
status 403:
|
|
144
|
+
status 403:
|
|
145
|
+
`this route needs a Caller, and a Sandbox token is not one: no Agent reaches run state, run control or a Gate`
|
|
146
|
+
- It refuses a Caller that does not have the `cancel` scope for the Workflow of the run with the status 403:
|
|
147
|
+
`the Caller <via>/<name> needs the "cancel" scope for Workflow "<workflow>" (ADR-0078)`
|
|
138
148
|
- A path with a prefix of the run id, or with an id of no run, gets `{"ok":true}` with the status 200. Nothing changes.
|
|
139
149
|
|
|
140
150
|
## Read next
|