appduct 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -52
- package/dist/artifact-inspect.d.ts +0 -1
- package/dist/bin.d.ts +0 -1
- package/dist/bin.js +1250 -2
- package/dist/cli/command-options.d.ts +8 -1
- package/dist/cli/create-cli.d.ts +0 -1
- package/dist/cli/dispatch.d.ts +10 -1
- package/dist/cli/envelope.d.ts +19 -0
- package/dist/cli/global-flags.d.ts +30 -0
- package/dist/cli/open-target.d.ts +25 -8
- package/dist/cli/result-types.d.ts +31 -5
- package/dist/cli/router.d.ts +78 -0
- package/dist/cli/routes/daemon/index.d.ts +9 -0
- package/dist/cli/routes/daemon/index.js +848 -0
- package/dist/cli/routes/daemon/run.d.ts +3 -0
- package/dist/cli/routes/daemon/run.js +3246 -0
- package/dist/cli/routes/daemon/start.d.ts +3 -0
- package/dist/cli/routes/daemon/start.js +1677 -0
- package/dist/cli/routes/daemon/status.d.ts +4 -0
- package/dist/cli/routes/daemon/status.js +1685 -0
- package/dist/cli/routes/daemon/stop.d.ts +4 -0
- package/dist/cli/routes/daemon/stop.js +1655 -0
- package/dist/cli/routes/doctor.d.ts +3 -0
- package/dist/cli/routes/doctor.js +1075 -0
- package/dist/cli/routes/events.d.ts +3 -0
- package/dist/cli/routes/events.js +1915 -0
- package/dist/cli/routes/init.d.ts +3 -0
- package/dist/cli/routes/init.js +1486 -0
- package/dist/cli/routes/invoke.d.ts +3 -0
- package/dist/cli/routes/invoke.js +1893 -0
- package/dist/cli/routes/keygen.d.ts +3 -0
- package/dist/cli/routes/keygen.js +928 -0
- package/dist/cli/routes/link.d.ts +3 -0
- package/dist/cli/routes/link.js +2714 -0
- package/dist/cli/routes/ls.d.ts +3 -0
- package/dist/cli/routes/ls.js +1668 -0
- package/dist/cli/routes/mcp.d.ts +5 -0
- package/dist/cli/routes/mcp.js +3929 -0
- package/dist/cli/routes/revoke.d.ts +3 -0
- package/dist/cli/routes/revoke.js +1684 -0
- package/dist/cli/routes/tools.d.ts +3 -0
- package/dist/cli/routes/tools.js +1860 -0
- package/dist/cli/runner.d.ts +3 -11
- package/dist/cli/types.d.ts +9 -6
- package/dist/cli/version-guard.d.ts +20 -0
- package/dist/cli.d.ts +0 -1
- package/dist/client/app-client.d.ts +4 -4
- package/dist/client/bootstrap.d.ts +4 -4
- package/dist/client/connect.d.ts +0 -1
- package/dist/client/errors.d.ts +0 -1
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.js +2389 -16
- package/dist/commands/daemon/context.d.ts +14 -0
- package/dist/commands/daemon/run.d.ts +11 -0
- package/dist/commands/daemon/start.d.ts +5 -0
- package/dist/commands/daemon/status.d.ts +5 -0
- package/dist/commands/daemon/stop.d.ts +5 -0
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/events.d.ts +0 -1
- package/dist/commands/init.d.ts +17 -4
- package/dist/commands/invoke.d.ts +0 -1
- package/dist/commands/keygen.d.ts +0 -1
- package/dist/commands/link.d.ts +1 -2
- package/dist/commands/ls.d.ts +0 -1
- package/dist/commands/mcp.d.ts +2 -4
- package/dist/commands/revoke.d.ts +0 -1
- package/dist/commands/tools.d.ts +19 -3
- package/dist/daemon/address.d.ts +0 -1
- package/dist/daemon/audit.d.ts +6 -11
- package/dist/daemon/calls.d.ts +0 -1
- package/dist/daemon/config.d.ts +13 -16
- package/dist/daemon/daemon.d.ts +0 -1
- package/dist/daemon/event-bus.d.ts +0 -1
- package/dist/daemon/links.d.ts +0 -1
- package/dist/daemon/listener.d.ts +0 -1
- package/dist/daemon/log-rotation.d.ts +0 -1
- package/dist/daemon/pidfile.d.ts +14 -8
- package/dist/daemon/policy.d.ts +0 -1
- package/dist/daemon/registry.d.ts +0 -1
- package/dist/daemon/rpc-errors.d.ts +16 -0
- package/dist/daemon/rpc-server.d.ts +0 -11
- package/dist/daemon/sessions.d.ts +0 -1
- package/dist/daemon/socket-probe.d.ts +0 -1
- package/dist/daemon/state-dir.d.ts +0 -1
- package/dist/daemon/timers.d.ts +0 -1
- package/dist/daemon/tls.d.ts +0 -1
- package/dist/errors.d.ts +0 -1
- package/dist/host-certificate.d.ts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1307 -3
- package/dist/key-material.d.ts +0 -1
- package/dist/link.d.ts +8 -7
- package/dist/mcp/app-tools.d.ts +153 -0
- package/dist/mcp/connect-tool.d.ts +14 -8
- package/dist/mcp/daemon-tools.d.ts +2 -11
- package/dist/mcp/events-tool.d.ts +0 -1
- package/dist/mcp/server.d.ts +33 -14
- package/dist/native-scheme.d.ts +0 -1
- package/dist/output.d.ts +8 -11
- package/dist/package-root.d.ts +11 -0
- package/dist/package-version.d.ts +0 -1
- package/dist/qr-terminal.d.ts +0 -1
- package/dist/rpc/client.d.ts +3 -3
- package/dist/scheme.d.ts +60 -2
- package/dist/spki-pin.d.ts +0 -1
- package/package.json +6 -3
- package/dist/artifact-inspect.d.ts.map +0 -1
- package/dist/artifact-inspect.js +0 -334
- package/dist/bin.d.ts.map +0 -1
- package/dist/cli/command-options.d.ts.map +0 -1
- package/dist/cli/command-options.js +0 -95
- package/dist/cli/create-cli.d.ts.map +0 -1
- package/dist/cli/create-cli.js +0 -56
- package/dist/cli/dispatch.d.ts.map +0 -1
- package/dist/cli/dispatch.js +0 -275
- package/dist/cli/open-target.d.ts.map +0 -1
- package/dist/cli/open-target.js +0 -438
- package/dist/cli/result-types.d.ts.map +0 -1
- package/dist/cli/result-types.js +0 -6
- package/dist/cli/runner.d.ts.map +0 -1
- package/dist/cli/runner.js +0 -111
- package/dist/cli/types.d.ts.map +0 -1
- package/dist/cli/types.js +0 -10
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client/app-client.d.ts.map +0 -1
- package/dist/client/app-client.js +0 -223
- package/dist/client/bootstrap.d.ts.map +0 -1
- package/dist/client/bootstrap.js +0 -108
- package/dist/client/connect.d.ts.map +0 -1
- package/dist/client/connect.js +0 -38
- package/dist/client/errors.d.ts.map +0 -1
- package/dist/client/errors.js +0 -40
- package/dist/client/index.d.ts.map +0 -1
- package/dist/commands/daemon.d.ts +0 -22
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -128
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -42
- package/dist/commands/events.d.ts.map +0 -1
- package/dist/commands/events.js +0 -76
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -231
- package/dist/commands/invoke.d.ts.map +0 -1
- package/dist/commands/invoke.js +0 -54
- package/dist/commands/keygen.d.ts.map +0 -1
- package/dist/commands/keygen.js +0 -46
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -43
- package/dist/commands/ls.d.ts.map +0 -1
- package/dist/commands/ls.js +0 -7
- package/dist/commands/mcp.d.ts.map +0 -1
- package/dist/commands/mcp.js +0 -98
- package/dist/commands/revoke.d.ts.map +0 -1
- package/dist/commands/revoke.js +0 -7
- package/dist/commands/tools.d.ts.map +0 -1
- package/dist/commands/tools.js +0 -52
- package/dist/daemon/address.d.ts.map +0 -1
- package/dist/daemon/address.js +0 -71
- package/dist/daemon/audit.d.ts.map +0 -1
- package/dist/daemon/audit.js +0 -237
- package/dist/daemon/calls.d.ts.map +0 -1
- package/dist/daemon/calls.js +0 -158
- package/dist/daemon/config.d.ts.map +0 -1
- package/dist/daemon/config.js +0 -170
- package/dist/daemon/daemon.d.ts.map +0 -1
- package/dist/daemon/daemon.js +0 -560
- package/dist/daemon/event-bus.d.ts.map +0 -1
- package/dist/daemon/event-bus.js +0 -111
- package/dist/daemon/links.d.ts.map +0 -1
- package/dist/daemon/links.js +0 -118
- package/dist/daemon/listener.d.ts.map +0 -1
- package/dist/daemon/listener.js +0 -132
- package/dist/daemon/log-rotation.d.ts.map +0 -1
- package/dist/daemon/log-rotation.js +0 -136
- package/dist/daemon/pidfile.d.ts.map +0 -1
- package/dist/daemon/pidfile.js +0 -90
- package/dist/daemon/policy.d.ts.map +0 -1
- package/dist/daemon/policy.js +0 -24
- package/dist/daemon/registry.d.ts.map +0 -1
- package/dist/daemon/registry.js +0 -31
- package/dist/daemon/rpc-server.d.ts.map +0 -1
- package/dist/daemon/rpc-server.js +0 -203
- package/dist/daemon/sessions.d.ts.map +0 -1
- package/dist/daemon/sessions.js +0 -429
- package/dist/daemon/socket-probe.d.ts.map +0 -1
- package/dist/daemon/socket-probe.js +0 -28
- package/dist/daemon/state-dir.d.ts.map +0 -1
- package/dist/daemon/state-dir.js +0 -43
- package/dist/daemon/timers.d.ts.map +0 -1
- package/dist/daemon/timers.js +0 -12
- package/dist/daemon/tls.d.ts.map +0 -1
- package/dist/daemon/tls.js +0 -79
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -162
- package/dist/host-certificate.d.ts.map +0 -1
- package/dist/host-certificate.js +0 -111
- package/dist/index.d.ts.map +0 -1
- package/dist/key-material.d.ts.map +0 -1
- package/dist/key-material.js +0 -42
- package/dist/link.d.ts.map +0 -1
- package/dist/link.js +0 -117
- package/dist/mcp/connect-tool.d.ts.map +0 -1
- package/dist/mcp/connect-tool.js +0 -396
- package/dist/mcp/daemon-tools.d.ts.map +0 -1
- package/dist/mcp/daemon-tools.js +0 -29
- package/dist/mcp/events-tool.d.ts.map +0 -1
- package/dist/mcp/events-tool.js +0 -322
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -538
- package/dist/mcp/tool-mapping.d.ts +0 -58
- package/dist/mcp/tool-mapping.d.ts.map +0 -1
- package/dist/mcp/tool-mapping.js +0 -143
- package/dist/mcp/tool-namespace.d.ts +0 -40
- package/dist/mcp/tool-namespace.d.ts.map +0 -1
- package/dist/mcp/tool-namespace.js +0 -65
- package/dist/native-scheme.d.ts.map +0 -1
- package/dist/native-scheme.js +0 -465
- package/dist/output.d.ts.map +0 -1
- package/dist/output.js +0 -342
- package/dist/package-version.d.ts.map +0 -1
- package/dist/package-version.js +0 -47
- package/dist/qr-terminal.d.ts.map +0 -1
- package/dist/qr-terminal.js +0 -45
- package/dist/rpc/client.d.ts.map +0 -1
- package/dist/rpc/client.js +0 -819
- package/dist/scheme.d.ts.map +0 -1
- package/dist/scheme.js +0 -321
- package/dist/spki-pin.d.ts.map +0 -1
- package/dist/spki-pin.js +0 -14
package/dist/mcp/connect-tool.js
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The built-in `appduct_connect` / `appduct_wait_for_session` management tools
|
|
3
|
-
* (ARCHITECTURE.md §9 scope item 4): let an MCP agent bootstrap a device session without shell
|
|
4
|
-
* access — mint a link (and, whenever a device can be found, deliver it via the task-07
|
|
5
|
-
* emulator/simulator fast path), then wait for it to be claimed.
|
|
6
|
-
*
|
|
7
|
-
* Delivery is attempted by *default*, not only on request: an agent that omitted `target` used to
|
|
8
|
-
* get a QR code, which is a flow that cannot complete without a human noticing and scanning it.
|
|
9
|
-
* The overwhelmingly common case — one booted simulator or emulator, no human watching — now just
|
|
10
|
-
* works, and the QR path is reserved for when there is genuinely nothing to deliver to (or the
|
|
11
|
-
* caller asked for it with `target: "none"`), where it carries explicit instructions telling the
|
|
12
|
-
* agent to show the code and ask.
|
|
13
|
-
*/
|
|
14
|
-
import { RPC_METHODS, } from "@appduct/shared";
|
|
15
|
-
import { deliverToOpenTarget, detectBootedTargets, isOpenTarget, isValidBundleId, invalidBundleIdMessage, isLoopbackAddress, loopbackAddressMessage, usesLoopbackAddress, MISSING_BUNDLE_ID_MESSAGE, } from "../cli/open-target.js";
|
|
16
|
-
import { composeDeepLink } from "../link.js";
|
|
17
|
-
import { renderQrToTerminal } from "../qr-terminal.js";
|
|
18
|
-
import { describeMissingScheme } from "../scheme.js";
|
|
19
|
-
import { openDaemonStream, DaemonRpcError, isDaemonUnreachableError, } from "../rpc/client.js";
|
|
20
|
-
export const CONNECT_TOOL_NAME = "appduct_connect";
|
|
21
|
-
export const WAIT_FOR_SESSION_TOOL_NAME = "appduct_wait_for_session";
|
|
22
|
-
/** The emulator/simulator fast path forces `127.0.0.1` — same reasoning as `cli/link.ts`. The
|
|
23
|
-
* experimental `ios-device` target is excluded (`usesLoopbackAddress`): a physical iPhone reaches
|
|
24
|
-
* the daemon only over the LAN. */
|
|
25
|
-
const OPEN_TARGET_ADDRESS_OVERRIDE = "127.0.0.1";
|
|
26
|
-
const DEFAULT_WAIT_TIMEOUT_MS = 120_000;
|
|
27
|
-
/** `target: "none"` — an explicit opt out of delivery, for when the human/QR flow is what is
|
|
28
|
-
* actually wanted (a physical device on the same network, say). Distinct from omitting `target`,
|
|
29
|
-
* which now means "figure it out". */
|
|
30
|
-
const NO_TARGET = "none";
|
|
31
|
-
/**
|
|
32
|
-
* Attached to every QR-path result. Without this an agent reliably minted a link, said nothing
|
|
33
|
-
* about it, and went straight to `appduct_wait_for_session` — which then blocked for its full
|
|
34
|
-
* timeout waiting on a scan nobody had been asked to perform.
|
|
35
|
-
*/
|
|
36
|
-
const QR_INSTRUCTIONS = "This link was NOT delivered to a device — a human has to open it. Before calling " +
|
|
37
|
-
"appduct_wait_for_session: show the user the \"qr\" field verbatim inside a fenced code block " +
|
|
38
|
-
"(it is pre-rendered terminal art — do not redraw, summarize, or describe it), show \"deepLink\" " +
|
|
39
|
-
"underneath it for a device that cannot scan, and ask the user to scan it with the target " +
|
|
40
|
-
"device. Only then call appduct_wait_for_session; it blocks until the device connects and " +
|
|
41
|
-
"gives no output while it waits, so calling it before you have asked will look like a hang.";
|
|
42
|
-
export const CONNECT_TOOL_DESCRIPTOR = {
|
|
43
|
-
name: CONNECT_TOOL_NAME,
|
|
44
|
-
description: "Mint an Appduct session link and get it onto a device. With no \"target\", auto-detects a " +
|
|
45
|
-
"booted iOS simulator or attached Android device and delivers the link to it, returning " +
|
|
46
|
-
"{ sessionId, delivered: true, autoDetected: true } — this is the normal agent path and needs " +
|
|
47
|
-
"no human. Pass target \"android\" or \"ios-sim\" (optionally with \"device\") to choose " +
|
|
48
|
-
"explicitly, or target \"none\" to force the human flow. Target \"ios-device\" is an " +
|
|
49
|
-
"experimental, never-auto-detected path that delivers to a paired physical iPhone/iPad via " +
|
|
50
|
-
"xcrun devicectl; it needs \"bundleId\" (or \"iosBundleId\" in config.json), iOS 17+, Xcode " +
|
|
51
|
-
"15+, a connected and trusted device with Developer Mode on, a dev-signed build installed, and " +
|
|
52
|
-
"the phone on the same network as this machine. Pass \"relaunch\": true with it if the app is " +
|
|
53
|
-
"already running and the delivery does not take. Only when no device is " +
|
|
54
|
-
"detected (or target is \"none\") does this return a QR code for a human to scan, along with " +
|
|
55
|
-
"an \"instructions\" field saying what to do with it. Follow up with " +
|
|
56
|
-
"appduct_wait_for_session to know when the device has connected.",
|
|
57
|
-
inputSchema: {
|
|
58
|
-
type: "object",
|
|
59
|
-
properties: {
|
|
60
|
-
target: { type: "string", enum: ["android", "ios-sim", "ios-device", NO_TARGET] },
|
|
61
|
-
device: { type: "string" },
|
|
62
|
-
bundleId: { type: "string" },
|
|
63
|
-
relaunch: { type: "boolean" },
|
|
64
|
-
ttlSeconds: { type: "number", exclusiveMinimum: 0 },
|
|
65
|
-
},
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
export const WAIT_FOR_SESSION_TOOL_DESCRIPTOR = {
|
|
70
|
-
name: WAIT_FOR_SESSION_TOOL_NAME,
|
|
71
|
-
description: "Wait for a session minted by appduct_connect to be claimed by a device. Resolves as soon " +
|
|
72
|
-
"as the device connects (or immediately if it already has); rejects with tool_timeout if " +
|
|
73
|
-
"timeoutMs elapses first. If appduct_connect returned a QR instead of delivering the link, " +
|
|
74
|
-
"show that QR to the user and ask them to scan it before calling this — it produces no output " +
|
|
75
|
-
"while it waits.",
|
|
76
|
-
inputSchema: {
|
|
77
|
-
type: "object",
|
|
78
|
-
properties: {
|
|
79
|
-
sessionId: { type: "string" },
|
|
80
|
-
timeoutMs: { type: "number", exclusiveMinimum: 0 },
|
|
81
|
-
},
|
|
82
|
-
required: ["sessionId"],
|
|
83
|
-
additionalProperties: false,
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
/** Errors from the built-in management tools reuse the daemon's wire `ErrorType` union
|
|
87
|
-
* (`RpcApplicationError`-style: `{ type, message }`) so the MCP server's generic error-content
|
|
88
|
-
* mapping (see `server.ts`) handles them the same way it handles a proxied device tool's error. */
|
|
89
|
-
export class McpBuiltinToolError extends Error {
|
|
90
|
-
type;
|
|
91
|
-
constructor(type, message) {
|
|
92
|
-
super(message);
|
|
93
|
-
this.type = type;
|
|
94
|
-
this.name = "McpBuiltinToolError";
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const asRecord = (value) => {
|
|
98
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
99
|
-
return {};
|
|
100
|
-
}
|
|
101
|
-
return value;
|
|
102
|
-
};
|
|
103
|
-
const asOptionalConnectTarget = (value) => {
|
|
104
|
-
if (value === undefined) {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
if (typeof value !== "string" || !(isOpenTarget(value) || value === NO_TARGET)) {
|
|
108
|
-
throw new McpBuiltinToolError("invalid_request", '"target" must be "android", "ios-sim", "ios-device", or "none".');
|
|
109
|
-
}
|
|
110
|
-
return value;
|
|
111
|
-
};
|
|
112
|
-
const asOptionalNonEmptyString = (value, field) => {
|
|
113
|
-
if (value === undefined) {
|
|
114
|
-
return undefined;
|
|
115
|
-
}
|
|
116
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
117
|
-
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a non-empty string.`);
|
|
118
|
-
}
|
|
119
|
-
return value;
|
|
120
|
-
};
|
|
121
|
-
const asOptionalBoolean = (value, field) => {
|
|
122
|
-
if (value === undefined) {
|
|
123
|
-
return undefined;
|
|
124
|
-
}
|
|
125
|
-
if (typeof value !== "boolean") {
|
|
126
|
-
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a boolean.`);
|
|
127
|
-
}
|
|
128
|
-
return value;
|
|
129
|
-
};
|
|
130
|
-
const asOptionalPositiveNumber = (value, field) => {
|
|
131
|
-
if (value === undefined) {
|
|
132
|
-
return undefined;
|
|
133
|
-
}
|
|
134
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
135
|
-
throw new McpBuiltinToolError("invalid_request", `"${field}" must be a positive number.`);
|
|
136
|
-
}
|
|
137
|
-
return value;
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* Decides where (if anywhere) this link is going, *before* it is minted: the `127.0.0.1` address
|
|
141
|
-
* override baked into a link depends on whether it is being delivered locally, and that cannot be
|
|
142
|
-
* revised after `link.create`. A link minted for loopback and then fallen back to a QR code would
|
|
143
|
-
* be unscannable-in-effect — the address would be wrong for the phone that scanned it.
|
|
144
|
-
*/
|
|
145
|
-
const resolveDelivery = async (requested, device, bundleId, deps) => {
|
|
146
|
-
if (requested !== undefined && requested !== NO_TARGET) {
|
|
147
|
-
// `ios-device` is reachable only through this branch: `detectBootedTargets` never returns it,
|
|
148
|
-
// so a paired iPhone can never be picked up by the auto-detection path below.
|
|
149
|
-
return {
|
|
150
|
-
delivery: {
|
|
151
|
-
target: requested,
|
|
152
|
-
device,
|
|
153
|
-
...(requested === "ios-device" ? { bundleId: bundleId ?? deps.iosBundleId } : {}),
|
|
154
|
-
label: device ?? requested,
|
|
155
|
-
autoDetected: false,
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
if (requested === NO_TARGET) {
|
|
160
|
-
return {
|
|
161
|
-
note: 'Called with target: "none", so no device delivery was attempted.',
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
const detection = await detectBootedTargets({ exec: deps.exec, env: deps.env });
|
|
165
|
-
if (detection.kind === "single") {
|
|
166
|
-
return { delivery: { ...detection.detected, autoDetected: true } };
|
|
167
|
-
}
|
|
168
|
-
if (detection.kind === "ambiguous") {
|
|
169
|
-
return {
|
|
170
|
-
note: 'No "target" was given and several devices are available (' +
|
|
171
|
-
detection.candidates.map((candidate) => `${candidate.target}: ${candidate.label}`).join("; ") +
|
|
172
|
-
'), so none was picked automatically. Re-call with "target" and "device" to deliver to one ' +
|
|
173
|
-
"of them, or use the QR below.",
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
note: 'No "target" was given and no booted iOS simulator or attached Android device was detected, ' +
|
|
178
|
-
"so the link could not be delivered automatically. A physical iPhone/iPad is never " +
|
|
179
|
-
'auto-detected: to deliver to one, re-call with target "ios-device" plus "bundleId" (needs ' +
|
|
180
|
-
"iOS 17+, Xcode 15+ and a paired, trusted device with Developer Mode on) — otherwise use " +
|
|
181
|
-
"the QR flow below.",
|
|
182
|
-
};
|
|
183
|
-
};
|
|
184
|
-
export const handleConnectTool = async (rawArgs, deps) => {
|
|
185
|
-
const args = asRecord(rawArgs);
|
|
186
|
-
const requestedTarget = asOptionalConnectTarget(args.target);
|
|
187
|
-
const device = asOptionalNonEmptyString(args.device, "device");
|
|
188
|
-
const bundleId = asOptionalNonEmptyString(args.bundleId, "bundleId");
|
|
189
|
-
const relaunch = asOptionalBoolean(args.relaunch, "relaunch");
|
|
190
|
-
const ttlSeconds = asOptionalPositiveNumber(args.ttlSeconds, "ttlSeconds");
|
|
191
|
-
if (device !== undefined && (requestedTarget === undefined || requestedTarget === NO_TARGET)) {
|
|
192
|
-
throw new McpBuiltinToolError("invalid_request", '"device" requires an explicit "target" of "android", "ios-sim" or "ios-device" — a bare ' +
|
|
193
|
-
"serial/udid does not say which platform it belongs to.");
|
|
194
|
-
}
|
|
195
|
-
if (bundleId !== undefined && requestedTarget !== "ios-device") {
|
|
196
|
-
throw new McpBuiltinToolError("invalid_request", '"bundleId" only applies with target "ios-device".');
|
|
197
|
-
}
|
|
198
|
-
if (relaunch !== undefined && requestedTarget !== "ios-device") {
|
|
199
|
-
throw new McpBuiltinToolError("invalid_request", '"relaunch" only applies with target "ios-device".');
|
|
200
|
-
}
|
|
201
|
-
if (!deps.scheme) {
|
|
202
|
-
throw new McpBuiltinToolError("invalid_request", describeMissingScheme(deps.schemeTried ?? []));
|
|
203
|
-
}
|
|
204
|
-
// Captured after the check so the closures below carry the narrowing (a mutable property does
|
|
205
|
-
// not stay narrowed across one).
|
|
206
|
-
const scheme = deps.scheme;
|
|
207
|
-
const { delivery, note } = await resolveDelivery(requestedTarget, device, bundleId, deps);
|
|
208
|
-
// Rejected before minting, so a call that cannot possibly deliver does not strand a pending
|
|
209
|
-
// session behind it.
|
|
210
|
-
if (delivery?.target === "ios-device" && !delivery.bundleId) {
|
|
211
|
-
throw new McpBuiltinToolError("invalid_request", MISSING_BUNDLE_ID_MESSAGE);
|
|
212
|
-
}
|
|
213
|
-
if (delivery?.bundleId !== undefined && !isValidBundleId(delivery.bundleId)) {
|
|
214
|
-
throw new McpBuiltinToolError("invalid_request", invalidBundleIdMessage(delivery.bundleId));
|
|
215
|
-
}
|
|
216
|
-
// A link's advertised address is fixed at mint time: `127.0.0.1` only when it is being handed to
|
|
217
|
-
// something that reaches this machine over loopback (an emulator or simulator — not a physical
|
|
218
|
-
// iPhone, which needs the LAN address). That is why delivery is decided first, and why the QR
|
|
219
|
-
// fallback below has to mint a *fresh* link rather than reuse a loopback one no phone could reach.
|
|
220
|
-
const mint = (deliverLocally) => {
|
|
221
|
-
return deps.call(RPC_METHODS.linkCreate, {
|
|
222
|
-
ttlSeconds,
|
|
223
|
-
addressOverride: deliverLocally ? OPEN_TARGET_ADDRESS_OVERRIDE : undefined,
|
|
224
|
-
});
|
|
225
|
-
};
|
|
226
|
-
const asQrResult = (result, qrNote) => {
|
|
227
|
-
const deepLink = composeDeepLink(scheme, result);
|
|
228
|
-
return {
|
|
229
|
-
sessionId: result.sessionId,
|
|
230
|
-
deepLink,
|
|
231
|
-
expiresAt: result.expiresAt,
|
|
232
|
-
...(qrNote === undefined ? {} : { note: qrNote }),
|
|
233
|
-
qr: renderQrToTerminal(deepLink),
|
|
234
|
-
instructions: QR_INSTRUCTIONS,
|
|
235
|
-
};
|
|
236
|
-
};
|
|
237
|
-
if (!delivery) {
|
|
238
|
-
return asQrResult(await mint(false), note);
|
|
239
|
-
}
|
|
240
|
-
const result = await mint(usesLoopbackAddress(delivery.target));
|
|
241
|
-
// Same guard as `link.ts`: `detectAdvertisedAddress` falls back to `127.0.0.1` when it finds no
|
|
242
|
-
// routable interface, and such a link points a physical phone at itself. Delivering it would
|
|
243
|
-
// leave `appduct_wait_for_session` blocking for its whole timeout with nothing to explain why.
|
|
244
|
-
if (delivery.target === "ios-device" && isLoopbackAddress(result.endpoint.address)) {
|
|
245
|
-
throw new McpBuiltinToolError("invalid_request", loopbackAddressMessage(result.endpoint.address));
|
|
246
|
-
}
|
|
247
|
-
const deepLink = composeDeepLink(scheme, result);
|
|
248
|
-
try {
|
|
249
|
-
await deliverToOpenTarget({
|
|
250
|
-
target: delivery.target,
|
|
251
|
-
device: delivery.device,
|
|
252
|
-
bundleId: delivery.bundleId,
|
|
253
|
-
relaunch,
|
|
254
|
-
deepLink,
|
|
255
|
-
wssPort: result.endpoint.port,
|
|
256
|
-
exec: deps.exec,
|
|
257
|
-
env: deps.env,
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
catch (error) {
|
|
261
|
-
const detail = error instanceof Error ? error.message : "Failed to deliver the deep link.";
|
|
262
|
-
// An explicitly named target that cannot be reached is the caller's problem to see: falling
|
|
263
|
-
// back would quietly ignore what they asked for.
|
|
264
|
-
if (!delivery.autoDetected) {
|
|
265
|
-
throw new McpBuiltinToolError("tool_execution_error", detail);
|
|
266
|
-
}
|
|
267
|
-
// An auto-detected one is not. Nobody asked for this device — a simulator that happens to be
|
|
268
|
-
// booted without the app installed, say — so a failure to reach it must not turn a call that
|
|
269
|
-
// would previously have returned a usable QR into a hard error. The loopback link just minted
|
|
270
|
-
// is abandoned (it expires on its own TTL) and a fresh, correctly-addressed one takes its
|
|
271
|
-
// place.
|
|
272
|
-
return asQrResult(await mint(false), `Auto-detected ${delivery.target} target ${delivery.label}, but delivering the link to it ` +
|
|
273
|
-
`failed: ${detail}. Falling back to a QR code — pass an explicit "target" (and "device") ` +
|
|
274
|
-
"if a different device is the right one.");
|
|
275
|
-
}
|
|
276
|
-
return {
|
|
277
|
-
sessionId: result.sessionId,
|
|
278
|
-
deepLink,
|
|
279
|
-
expiresAt: result.expiresAt,
|
|
280
|
-
delivered: true,
|
|
281
|
-
target: delivery.target,
|
|
282
|
-
...(delivery.device === undefined ? {} : { device: delivery.device }),
|
|
283
|
-
...(delivery.autoDetected
|
|
284
|
-
? {
|
|
285
|
-
autoDetected: true,
|
|
286
|
-
note: `No "target" was given; delivered to the only device found: ${delivery.label}.`,
|
|
287
|
-
}
|
|
288
|
-
: {}),
|
|
289
|
-
};
|
|
290
|
-
};
|
|
291
|
-
/** Socket-level failures that mean the daemon went away mid-wait. `isDaemonUnreachableError`
|
|
292
|
-
* covers the "never got there" cases (`ENOENT`/`ECONNREFUSED`) and a timed-out request; these are
|
|
293
|
-
* the "was there, then wasn't" ones, which surface raw from a call that was already in flight. */
|
|
294
|
-
const CONNECTION_LOST_CODES = new Set(["ECONNRESET", "EPIPE"]);
|
|
295
|
-
/** Appends the underlying cause, so an operator reading CI output still gets the syscall detail
|
|
296
|
-
* even though the sentence in front of it is what the agent acts on. */
|
|
297
|
-
const describeCause = (error) => {
|
|
298
|
-
return error instanceof Error && error.message.length > 0 ? ` (${error.message})` : "";
|
|
299
|
-
};
|
|
300
|
-
const daemonClosedMessage = (sessionId) => `The connection to the Appduct daemon closed while waiting for session "${sessionId}".`;
|
|
301
|
-
const daemonUnreachableMessage = (sessionId, error) => `The Appduct daemon is unreachable, so session "${sessionId}" cannot be waited on${describeCause(error)}.`;
|
|
302
|
-
const isConnectionLost = (error) => {
|
|
303
|
-
if (isDaemonUnreachableError(error)) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
const code = error?.code;
|
|
307
|
-
return typeof code === "string" && CONNECTION_LOST_CODES.has(code);
|
|
308
|
-
};
|
|
309
|
-
export const handleWaitForSessionTool = async (rawArgs, deps) => {
|
|
310
|
-
const args = asRecord(rawArgs);
|
|
311
|
-
if (typeof args.sessionId !== "string" || args.sessionId.length === 0) {
|
|
312
|
-
throw new McpBuiltinToolError("invalid_request", '"sessionId" must be a non-empty string.');
|
|
313
|
-
}
|
|
314
|
-
const sessionId = args.sessionId;
|
|
315
|
-
const timeoutMs = asOptionalPositiveNumber(args.timeoutMs, "timeoutMs") ?? DEFAULT_WAIT_TIMEOUT_MS;
|
|
316
|
-
let stream;
|
|
317
|
-
try {
|
|
318
|
-
stream = await openDaemonStream({ stateDir: deps.stateDir, spawn: deps.spawn });
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
// The daemon can be on its way out at the exact moment this tool opens its stream, in which
|
|
322
|
-
// case `connect` fails with ECONNRESET — not the ENOENT/ECONNREFUSED that would mean "not
|
|
323
|
-
// running" and trigger an auto-spawn. That is the same situation as losing it mid-wait, and
|
|
324
|
-
// has to read the same way rather than as a bare syscall error against a socket path.
|
|
325
|
-
if (isConnectionLost(error)) {
|
|
326
|
-
throw new McpBuiltinToolError("tool_execution_error", daemonUnreachableMessage(sessionId, error));
|
|
327
|
-
}
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
return await new Promise((resolve, reject) => {
|
|
332
|
-
let settled = false;
|
|
333
|
-
const settle = (fn) => {
|
|
334
|
-
if (settled) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
settled = true;
|
|
338
|
-
clearTimeout(timer);
|
|
339
|
-
unsubscribeNotification();
|
|
340
|
-
unsubscribeClose();
|
|
341
|
-
fn();
|
|
342
|
-
};
|
|
343
|
-
const timer = setTimeout(() => {
|
|
344
|
-
settle(() => reject(new McpBuiltinToolError("tool_timeout", `Timed out after ${timeoutMs}ms waiting for session "${sessionId}" to be claimed.`)));
|
|
345
|
-
}, timeoutMs);
|
|
346
|
-
const connectionLost = () => new McpBuiltinToolError("tool_execution_error", daemonClosedMessage(sessionId));
|
|
347
|
-
// A daemon that goes away mid-wait must not read as "still waiting" for the rest of the
|
|
348
|
-
// timeout — the caller has to know the connection is gone, not sit in silence.
|
|
349
|
-
const unsubscribeClose = stream.onClose(() => {
|
|
350
|
-
settle(() => reject(connectionLost()));
|
|
351
|
-
});
|
|
352
|
-
// Registered *before* `events.subscribe` is sent (not after awaiting it) — a
|
|
353
|
-
// `session_claimed` notification that arrives in the same TCP chunk as the subscribe
|
|
354
|
-
// response is dispatched synchronously within that chunk's processing, before an `await`
|
|
355
|
-
// continuation would get a chance to run; registering first means it is always in place in
|
|
356
|
-
// time to see it. Same reasoning as `client/bootstrap.ts`.
|
|
357
|
-
const unsubscribeNotification = stream.onNotification((payload) => {
|
|
358
|
-
const event = payload;
|
|
359
|
-
if (event.kind === "session_claimed" && event.sessionId === sessionId) {
|
|
360
|
-
settle(() => resolve({ sessionId, claimed: true, alias: event.alias ?? "" }));
|
|
361
|
-
}
|
|
362
|
-
});
|
|
363
|
-
stream
|
|
364
|
-
.call(RPC_METHODS.eventsSubscribe, { sessionSelector: sessionId, kinds: ["session_claimed"] })
|
|
365
|
-
.then(async () => {
|
|
366
|
-
if (settled) {
|
|
367
|
-
return;
|
|
368
|
-
}
|
|
369
|
-
// Catch-up, once the subscription is definitely in place: the session may have been
|
|
370
|
-
// claimed before this tool ran at all (an agent polling, or a long gap after
|
|
371
|
-
// appduct_connect), or in the round trip the subscribe itself took. Either way the
|
|
372
|
-
// event is already in the past and no amount of waiting will replay it.
|
|
373
|
-
const existing = await stream.call(RPC_METHODS.sessionsDescribe, {
|
|
374
|
-
selector: sessionId,
|
|
375
|
-
});
|
|
376
|
-
settle(() => resolve({ sessionId, claimed: true, alias: existing.alias }));
|
|
377
|
-
})
|
|
378
|
-
.catch((error) => {
|
|
379
|
-
// `unknown_session` from the catch-up is the expected "not claimed yet" answer — keep
|
|
380
|
-
// waiting for the live event. Anything else is a real failure.
|
|
381
|
-
if (error instanceof DaemonRpcError && error.data?.type === "unknown_session") {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
// A daemon dying mid-wait races two routes to get here: `onClose` above, or whichever
|
|
385
|
-
// call was in flight rejecting with a raw socket error. Same condition, so report it the
|
|
386
|
-
// same way — which route wins is a platform detail (Linux tends to reset where macOS
|
|
387
|
-
// closes), and nobody should have to read `connect ECONNRESET <socket path>` to find out
|
|
388
|
-
// the daemon stopped.
|
|
389
|
-
settle(() => reject(isConnectionLost(error) ? connectionLost() : error));
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
finally {
|
|
394
|
-
stream.close();
|
|
395
|
-
}
|
|
396
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"daemon-tools.d.ts","sourceRoot":"","sources":["../../src/mcp/daemon-tools.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAUH,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAEhF;mDACmD;AACnD,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEzF,eAAO,MAAM,mBAAmB,GAAU,MAAM,UAAU,KAAG,OAAO,CAAC,cAAc,EAAE,CAuBpF,CAAC"}
|
package/dist/mcp/daemon-tools.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fetches the effective (possibly namespaced) MCP tool list live from the daemon
|
|
3
|
-
* (ARCHITECTURE.md §9): `sessions.list` for the live sessions, then `tools.list` per session.
|
|
4
|
-
* There is no caching here — every `tools/list` request and every list-changed check re-fetches,
|
|
5
|
-
* since the daemon's session/registry state is the single source of truth and can change between
|
|
6
|
-
* any two calls.
|
|
7
|
-
*/
|
|
8
|
-
import { RPC_METHODS, } from "@appduct/shared";
|
|
9
|
-
import { DaemonRpcError } from "../rpc/client.js";
|
|
10
|
-
import { buildNamespacedTools } from "./tool-namespace.js";
|
|
11
|
-
export const fetchEffectiveTools = async (call) => {
|
|
12
|
-
const sessions = await call(RPC_METHODS.sessionsList);
|
|
13
|
-
const toolsByAlias = new Map();
|
|
14
|
-
for (const session of sessions) {
|
|
15
|
-
try {
|
|
16
|
-
toolsByAlias.set(session.alias, await call(RPC_METHODS.toolsList, { selector: session.alias }));
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
// The session can transition (revoke/expire) between `sessions.list` and this per-session
|
|
20
|
-
// `tools.list` call; treat it as having no tools rather than failing the whole listing.
|
|
21
|
-
if (error instanceof DaemonRpcError) {
|
|
22
|
-
toolsByAlias.set(session.alias, []);
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
throw error;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return buildNamespacedTools(sessions, toolsByAlias);
|
|
29
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"events-tool.d.ts","sourceRoot":"","sources":["../../src/mcp/events-tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAKL,KAAK,iBAAiB,EAEvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AACjD,eAAO,MAAM,wBAAwB,2BAA2B,CAAC;AAcjE;;sFAEsF;AACtF,eAAO,MAAM,0BAA0B,GAAI,aAAa,MAAM,GAAG,SAAS,KAAG,MAE5E,CAAC;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoBzB,CAAC;AAEX,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BjC,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AA0GF,eAAO,MAAM,gBAAgB,GAAU,SAAS,OAAO,EAAE,MAAM,cAAc,KAAG,OAAO,CAAC,iBAAiB,CASxG,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB;oEACgE;IAChE,QAAQ,CAAC,EAAE;QACT,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QACvB,gBAAgB,EAAE,CAAC,YAAY,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;KAC5D,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe;IACf,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AAYF,eAAO,MAAM,sBAAsB,GACjC,SAAS,OAAO,EAChB,MAAM,oBAAoB,KACzB,OAAO,CAAC,sBAAsB,CA6LhC,CAAC"}
|