@beeos-ai/device-mcp-server 0.3.0 → 0.4.2
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/dist/backends/android-adb.d.ts +147 -6
- package/dist/backends/android-adb.js +776 -40
- package/dist/backends/android-adb.js.map +1 -1
- package/dist/backends/base.d.ts +243 -7
- package/dist/backends/base.js +81 -2
- package/dist/backends/base.js.map +1 -1
- package/dist/backends/desktop.d.ts +3 -2
- package/dist/backends/desktop.js +9 -3
- package/dist/backends/desktop.js.map +1 -1
- package/dist/backends/linux.js +3 -0
- package/dist/backends/linux.js.map +1 -1
- package/dist/backends/mac.d.ts +11 -2
- package/dist/backends/mac.js +39 -1
- package/dist/backends/mac.js.map +1 -1
- package/dist/backends/stubs/windows.js +3 -0
- package/dist/backends/stubs/windows.js.map +1 -1
- package/dist/cli.d.ts +40 -26
- package/dist/cli.js +118 -84
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/server/app.d.ts +60 -17
- package/dist/server/app.js +182 -138
- package/dist/server/app.js.map +1 -1
- package/dist/server/mcp-server.d.ts +25 -0
- package/dist/server/mcp-server.js +33 -0
- package/dist/server/mcp-server.js.map +1 -0
- package/dist/server/registry.d.ts +111 -0
- package/dist/server/registry.js +191 -0
- package/dist/server/registry.js.map +1 -0
- package/dist/server/stdio.d.ts +29 -0
- package/dist/server/stdio.js +35 -0
- package/dist/server/stdio.js.map +1 -0
- package/dist/server/tool-registry.d.ts +60 -35
- package/dist/server/tool-registry.js +911 -434
- package/dist/server/tool-registry.js.map +1 -1
- package/dist/util/adb-files.d.ts +25 -1
- package/dist/util/adb-files.js +95 -0
- package/dist/util/adb-files.js.map +1 -1
- package/dist/util/locale.d.ts +16 -0
- package/dist/util/locale.js +31 -0
- package/dist/util/locale.js.map +1 -0
- package/dist/util/logger.d.ts +27 -0
- package/dist/util/logger.js +27 -0
- package/dist/util/logger.js.map +1 -0
- package/dist/util/output-path.d.ts +60 -0
- package/dist/util/output-path.js +123 -0
- package/dist/util/output-path.js.map +1 -0
- package/dist/util/package-name.d.ts +26 -0
- package/dist/util/package-name.js +41 -0
- package/dist/util/package-name.js.map +1 -0
- package/package.json +5 -3
- package/dist/server/action-mapping.d.ts +0 -21
- package/dist/server/action-mapping.js +0 -153
- package/dist/server/action-mapping.js.map +0 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackendRegistry — manages multiple `SandboxBackend` instances inside a
|
|
3
|
+
* **single** device-mcp-server process.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the legacy "one process per device" topology (mirrored from the
|
|
6
|
+
* `mobile-mcp` design): a single `device-mcp-server` discovers every adb device
|
|
7
|
+
* in the host's `adb devices` output and lazily wraps each one in an
|
|
8
|
+
* `AndroidAdbBackend`. Tool handlers resolve the right backend through three
|
|
9
|
+
* fallbacks (in priority order):
|
|
10
|
+
*
|
|
11
|
+
* 1. explicit `device` argument on the tool call (`device: "emulator-5554"`)
|
|
12
|
+
* 2. session-sticky default set via the `use_device` MCP tool (keyed by
|
|
13
|
+
* `extra.sessionId`, only meaningful for stdio / single-session clients)
|
|
14
|
+
* 3. single-device shortcut: if exactly one device is connected, use it
|
|
15
|
+
*
|
|
16
|
+
* If none of the above resolves to a backend the registry throws
|
|
17
|
+
* `DeviceError("device required", subtype:"invalid_args")` so the MCP tool
|
|
18
|
+
* call returns a structured error instead of silently picking a random device.
|
|
19
|
+
*
|
|
20
|
+
* The registry also owns a periodic `adb devices` poll (default 10s) so newly
|
|
21
|
+
* plugged devices show up in `list_available_devices` without restarting the
|
|
22
|
+
* process. Connect-state validation per backend is lazy — backends are
|
|
23
|
+
* constructed with `skipConnectValidation: true` and only emit errors at
|
|
24
|
+
* tool-call time.
|
|
25
|
+
*
|
|
26
|
+
* Desktop / stub backends are registered up front (one each) and re-used
|
|
27
|
+
* across every session — the device id for a desktop backend is its
|
|
28
|
+
* `os` string (`"desktop-macos"`, `"desktop-linux"`, ...).
|
|
29
|
+
*/
|
|
30
|
+
import { DeviceError } from "@beeos-ai/device-common";
|
|
31
|
+
import { listAdbDevices, AndroidAdbBackend } from "../backends/android-adb.js";
|
|
32
|
+
export class BackendRegistry {
|
|
33
|
+
backends = new Map();
|
|
34
|
+
enableAdbDiscovery;
|
|
35
|
+
adbRunner;
|
|
36
|
+
pollIntervalMs;
|
|
37
|
+
androidDefaults;
|
|
38
|
+
pollTimer;
|
|
39
|
+
/**
|
|
40
|
+
* Per-session active-device map (only meaningful for stdio / single-session
|
|
41
|
+
* transports). For stateless HTTP `extra.sessionId` is undefined and this
|
|
42
|
+
* map is never written.
|
|
43
|
+
*/
|
|
44
|
+
sessionDevice = new Map();
|
|
45
|
+
constructor(opts = {}) {
|
|
46
|
+
this.enableAdbDiscovery = opts.enableAdbDiscovery ?? true;
|
|
47
|
+
this.adbRunner = opts.adbRunner;
|
|
48
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 10_000;
|
|
49
|
+
this.androidDefaults = opts.androidDefaults;
|
|
50
|
+
for (const b of opts.initialBackends ?? []) {
|
|
51
|
+
// Static (desktop / stub) backends — id == os.
|
|
52
|
+
this.backends.set(b.os, b);
|
|
53
|
+
}
|
|
54
|
+
for (const serial of opts.initialAdbSerials ?? []) {
|
|
55
|
+
this.backends.set(serial, this.makeAdbBackend(serial));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Kick off the adb-discovery poll and run one immediate scan. Returns
|
|
60
|
+
* after the first scan completes (so callers can `await` and immediately
|
|
61
|
+
* see a populated `list()`).
|
|
62
|
+
*
|
|
63
|
+
* Idempotent — safe to call multiple times.
|
|
64
|
+
*/
|
|
65
|
+
async start() {
|
|
66
|
+
if (!this.enableAdbDiscovery)
|
|
67
|
+
return;
|
|
68
|
+
await this.pollOnce().catch(() => {
|
|
69
|
+
/* swallow — next poll will surface errors as device disappearance */
|
|
70
|
+
});
|
|
71
|
+
if (this.pollTimer)
|
|
72
|
+
return;
|
|
73
|
+
this.pollTimer = setInterval(() => {
|
|
74
|
+
void this.pollOnce().catch(() => undefined);
|
|
75
|
+
}, this.pollIntervalMs);
|
|
76
|
+
// Don't keep the event loop alive for the poll alone.
|
|
77
|
+
this.pollTimer.unref?.();
|
|
78
|
+
}
|
|
79
|
+
/** Stop the discovery poll and disconnect every backend. */
|
|
80
|
+
async stop() {
|
|
81
|
+
if (this.pollTimer) {
|
|
82
|
+
clearInterval(this.pollTimer);
|
|
83
|
+
this.pollTimer = undefined;
|
|
84
|
+
}
|
|
85
|
+
await Promise.all([...this.backends.values()].map((b) => b.disconnect().catch(() => undefined)));
|
|
86
|
+
}
|
|
87
|
+
/** Snapshot of every currently-known device. */
|
|
88
|
+
list() {
|
|
89
|
+
const ids = [...this.backends.keys()];
|
|
90
|
+
const isSingle = ids.length === 1;
|
|
91
|
+
return ids.map((id) => {
|
|
92
|
+
const b = this.backends.get(id);
|
|
93
|
+
return {
|
|
94
|
+
id,
|
|
95
|
+
os: b.os,
|
|
96
|
+
isDefault: isSingle,
|
|
97
|
+
source: b.os === "android" ? "adb" : "desktop",
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a backend for a single tool call. `args.device` wins; if absent,
|
|
103
|
+
* fall back to the session-sticky `use_device` value; if also absent and
|
|
104
|
+
* exactly one backend is registered, use it. Otherwise throw with a clear
|
|
105
|
+
* "device required" error so the MCP client knows it must call
|
|
106
|
+
* `list_available_devices` / `use_device` first.
|
|
107
|
+
*/
|
|
108
|
+
resolve(args, sessionId) {
|
|
109
|
+
const explicit = typeof args?.device === "string" ? args.device : undefined;
|
|
110
|
+
if (explicit) {
|
|
111
|
+
const b = this.backends.get(explicit);
|
|
112
|
+
if (!b) {
|
|
113
|
+
throw new DeviceError(`device '${explicit}' is not connected; call list_available_devices`, { subtype: "invalid_args", retriable: false });
|
|
114
|
+
}
|
|
115
|
+
return b;
|
|
116
|
+
}
|
|
117
|
+
if (sessionId) {
|
|
118
|
+
const sticky = this.sessionDevice.get(sessionId);
|
|
119
|
+
if (sticky) {
|
|
120
|
+
const b = this.backends.get(sticky);
|
|
121
|
+
if (b)
|
|
122
|
+
return b;
|
|
123
|
+
// sticky reference is stale — drop it so the caller gets the
|
|
124
|
+
// "device required" message instead of a stale-id error.
|
|
125
|
+
this.sessionDevice.delete(sessionId);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (this.backends.size === 1) {
|
|
129
|
+
return this.backends.values().next().value;
|
|
130
|
+
}
|
|
131
|
+
if (this.backends.size === 0) {
|
|
132
|
+
throw new DeviceError("no devices connected; plug in a device or call list_available_devices", { subtype: "no_device", retriable: true });
|
|
133
|
+
}
|
|
134
|
+
throw new DeviceError("multiple devices connected; pass `device: <id>` or call use_device first", { subtype: "invalid_args", retriable: false });
|
|
135
|
+
}
|
|
136
|
+
/** Set the session-sticky default device. Used by the `use_device` tool. */
|
|
137
|
+
useDevice(sessionId, deviceId) {
|
|
138
|
+
if (!this.backends.has(deviceId)) {
|
|
139
|
+
throw new DeviceError(`device '${deviceId}' is not connected; call list_available_devices`, { subtype: "invalid_args", retriable: false });
|
|
140
|
+
}
|
|
141
|
+
if (sessionId) {
|
|
142
|
+
this.sessionDevice.set(sessionId, deviceId);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// No session = HTTP stateless. Treat as a no-op + structured error so
|
|
146
|
+
// clients learn the "always pass `device`" rule explicitly.
|
|
147
|
+
throw new DeviceError("use_device has no effect over stateless HTTP — pass `device` on every tool call instead", { subtype: "invalid_args", retriable: false });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async pollOnce() {
|
|
151
|
+
let serials = [];
|
|
152
|
+
try {
|
|
153
|
+
serials = await listAdbDevices(this.adbRunner);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
// adb unavailable on this host — nothing to add. Static (desktop)
|
|
157
|
+
// backends seeded at construction time stay registered.
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const seen = new Set(serials);
|
|
161
|
+
for (const serial of serials) {
|
|
162
|
+
if (!this.backends.has(serial)) {
|
|
163
|
+
this.backends.set(serial, this.makeAdbBackend(serial));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Prune adb backends whose devices have disappeared. Static (desktop)
|
|
167
|
+
// backends are never adb serials so they're untouched by this sweep.
|
|
168
|
+
for (const [id, backend] of this.backends.entries()) {
|
|
169
|
+
if (backend.os === "android" && !seen.has(id)) {
|
|
170
|
+
this.backends.delete(id);
|
|
171
|
+
backend.disconnect().catch(() => undefined);
|
|
172
|
+
// Drop session-sticky pointers to the removed device.
|
|
173
|
+
for (const [sid, dev] of this.sessionDevice.entries()) {
|
|
174
|
+
if (dev === id)
|
|
175
|
+
this.sessionDevice.delete(sid);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
makeAdbBackend(serial) {
|
|
181
|
+
return new AndroidAdbBackend({
|
|
182
|
+
...this.androidDefaults,
|
|
183
|
+
serial,
|
|
184
|
+
// Lazy connect — discovery already proved the device is in `adb
|
|
185
|
+
// devices`, so a per-backend `adb devices` validation is redundant
|
|
186
|
+
// and would add startup latency proportional to device count.
|
|
187
|
+
skipConnectValidation: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/server/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio transport for `device-mcp-server`.
|
|
3
|
+
*
|
|
4
|
+
* Used when the server is launched as a child process by an MCP client
|
|
5
|
+
* (Cursor, Claude Desktop, custom orchestrators). Single persistent
|
|
6
|
+
* connection over stdin/stdout — `extra.sessionId` is stable for the
|
|
7
|
+
* lifetime of the process so `use_device` / `list_available_devices`
|
|
8
|
+
* give a `mobile-mcp`-style stateful UX.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: nothing may write to stdout in stdio mode — that channel
|
|
11
|
+
* is reserved for JSON-RPC frames. All logging goes through `stderr`
|
|
12
|
+
* via `util/logger`. The `runStdio()` function intentionally does NOT
|
|
13
|
+
* `console.log` or `process.stdout.write` anything.
|
|
14
|
+
*/
|
|
15
|
+
import type { BackendRegistry } from "./registry.js";
|
|
16
|
+
import { type Logger } from "../util/logger.js";
|
|
17
|
+
export interface RunStdioOptions {
|
|
18
|
+
registry: BackendRegistry;
|
|
19
|
+
name?: string;
|
|
20
|
+
version?: string;
|
|
21
|
+
logger?: Logger;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Connect a fresh `McpServer` to stdin/stdout. Resolves once the
|
|
25
|
+
* transport is wired up — the process keeps running because the SDK
|
|
26
|
+
* holds the stdin readable open. Callers should let `process.exit`
|
|
27
|
+
* happen naturally on stdin EOF.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runStdio(opts: RunStdioOptions): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stdio transport for `device-mcp-server`.
|
|
3
|
+
*
|
|
4
|
+
* Used when the server is launched as a child process by an MCP client
|
|
5
|
+
* (Cursor, Claude Desktop, custom orchestrators). Single persistent
|
|
6
|
+
* connection over stdin/stdout — `extra.sessionId` is stable for the
|
|
7
|
+
* lifetime of the process so `use_device` / `list_available_devices`
|
|
8
|
+
* give a `mobile-mcp`-style stateful UX.
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: nothing may write to stdout in stdio mode — that channel
|
|
11
|
+
* is reserved for JSON-RPC frames. All logging goes through `stderr`
|
|
12
|
+
* via `util/logger`. The `runStdio()` function intentionally does NOT
|
|
13
|
+
* `console.log` or `process.stdout.write` anything.
|
|
14
|
+
*/
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import { buildMcpServer } from "./mcp-server.js";
|
|
17
|
+
import { stderrLogger } from "../util/logger.js";
|
|
18
|
+
/**
|
|
19
|
+
* Connect a fresh `McpServer` to stdin/stdout. Resolves once the
|
|
20
|
+
* transport is wired up — the process keeps running because the SDK
|
|
21
|
+
* holds the stdin readable open. Callers should let `process.exit`
|
|
22
|
+
* happen naturally on stdin EOF.
|
|
23
|
+
*/
|
|
24
|
+
export async function runStdio(opts) {
|
|
25
|
+
const log = opts.logger ?? stderrLogger;
|
|
26
|
+
const server = buildMcpServer({
|
|
27
|
+
registry: opts.registry,
|
|
28
|
+
name: opts.name,
|
|
29
|
+
version: opts.version,
|
|
30
|
+
});
|
|
31
|
+
const transport = new StdioServerTransport();
|
|
32
|
+
await server.connect(transport);
|
|
33
|
+
log.info?.({ name: opts.name ?? "device-mcp-server", version: opts.version }, "stdio_ready");
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=stdio.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio.js","sourceRoot":"","sources":["../../src/server/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,EAAE,YAAY,EAAe,MAAM,mBAAmB,CAAC;AAS9D;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAqB;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,IAAI,YAAY,CAAC;IACxC,MAAM,MAAM,GAAG,cAAc,CAAC;QAC5B,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,IAAI,CAAC,OAAO;KACtB,CAAC,CAAC;IACH,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,GAAG,CAAC,IAAI,EAAE,CACR,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,mBAAmB,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EACjE,aAAa,CACd,CAAC;AACJ,CAAC"}
|
|
@@ -1,50 +1,75 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* SDK-style tool catalogue for device-mcp-server.
|
|
3
3
|
*
|
|
4
|
-
* Each tool
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Each tool is a `ToolSpec` describing:
|
|
5
|
+
* - `name` MCP tool identifier (`screenshot`, `tap`, ...)
|
|
6
|
+
* - `description` human-readable verb
|
|
7
|
+
* - `inputShape` zod raw shape — every shape gets an implicit
|
|
8
|
+
* `device?: z.string().optional()` injected by
|
|
9
|
+
* `registerAllTools` so multi-device callers can
|
|
10
|
+
* target a specific backend per call
|
|
11
|
+
* - `annotations` `ToolAnnotations` (read-only / destructive /
|
|
12
|
+
* idempotent / open-world hints)
|
|
13
|
+
* - `requires` MCP tool name the resolved backend must declare
|
|
14
|
+
* in `backend.tools`. Tools with `requires:
|
|
15
|
+
* "registry"` are registry-level (don't touch a
|
|
16
|
+
* backend) — used for `list_available_devices` /
|
|
17
|
+
* `use_device`.
|
|
18
|
+
* - `handler` async `(args, ctx) => CallToolResult`
|
|
8
19
|
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
* support gates of its own. `listToolDescriptorsFor(backend)` filters
|
|
13
|
-
* via `backend.tools.has(name)`; `getToolFor(name, backend)` mirrors
|
|
14
|
-
* the same gate so `/mcp/tools/call` returns 404 (instead of 200 +
|
|
15
|
-
* runtime `unsupported`) for any tool the active backend doesn't
|
|
16
|
-
* advertise.
|
|
20
|
+
* `registerAllTools(server, registry)` iterates the catalogue and calls
|
|
21
|
+
* `server.registerTool` for each entry. The handler closure captures the
|
|
22
|
+
* registry so it can resolve the per-call backend via `registry.resolve`.
|
|
17
23
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
24
|
+
* Returning shapes:
|
|
25
|
+
* - Every handler returns `{ content, structuredContent }`. `content`
|
|
26
|
+
* carries the human-friendly text summary (printable in MCP clients
|
|
27
|
+
* like Claude Desktop / Cursor); `structuredContent` carries the
|
|
28
|
+
* typed payload that programmatic callers (`device-agent`) parse.
|
|
20
29
|
*/
|
|
21
|
-
import
|
|
30
|
+
import { z } from "zod";
|
|
31
|
+
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
32
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
22
33
|
import type { SandboxBackend } from "../backends/base.js";
|
|
34
|
+
import type { BackendRegistry } from "./registry.js";
|
|
35
|
+
/** Backend reference passed into handlers for backend-bound tools. */
|
|
23
36
|
export interface ToolHandlerContext {
|
|
24
37
|
backend: SandboxBackend;
|
|
38
|
+
registry: BackendRegistry;
|
|
39
|
+
sessionId?: string;
|
|
25
40
|
}
|
|
26
|
-
export type ToolHandler = (args: Record<string, unknown>, ctx: ToolHandlerContext) => Promise<
|
|
27
|
-
export interface
|
|
28
|
-
|
|
41
|
+
export type ToolHandler = (args: Record<string, unknown>, ctx: ToolHandlerContext) => Promise<CallToolResult>;
|
|
42
|
+
export interface ToolSpec {
|
|
43
|
+
name: string;
|
|
44
|
+
description: string;
|
|
45
|
+
/** Zod raw shape (NO `device` — the registrar injects it automatically). */
|
|
46
|
+
inputShape: z.ZodRawShape;
|
|
47
|
+
annotations?: ToolAnnotations;
|
|
48
|
+
/**
|
|
49
|
+
* Backend tool gate. Either an MCP tool name (must appear in
|
|
50
|
+
* `backend.tools`) or `"registry"` for registry-level tools that don't
|
|
51
|
+
* need a backend at all.
|
|
52
|
+
*/
|
|
53
|
+
requires: string | "registry";
|
|
29
54
|
handler: ToolHandler;
|
|
30
55
|
}
|
|
31
|
-
export declare const
|
|
56
|
+
export declare const TOOL_SPECS: ToolSpec[];
|
|
32
57
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* Useful for documentation / tests that want to enumerate the catalog.
|
|
36
|
-
* Wire callers SHOULD use `listToolDescriptorsFor(backend)` so the list
|
|
37
|
-
* only contains tools the active backend can actually execute.
|
|
58
|
+
* Look up a tool spec by name. Used by tests / introspection — production
|
|
59
|
+
* code goes through `registerAllTools(server, registry)`.
|
|
38
60
|
*/
|
|
39
|
-
export declare function
|
|
40
|
-
/**
|
|
41
|
-
export declare function
|
|
42
|
-
/** Look up a tool by name, ignoring backend tool-set gates. */
|
|
43
|
-
export declare function getTool(name: string): ToolEntry | undefined;
|
|
61
|
+
export declare function getToolSpec(name: string): ToolSpec | undefined;
|
|
62
|
+
/** Ordered list of every tool name advertised by the server. */
|
|
63
|
+
export declare function allToolNames(): string[];
|
|
44
64
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* Register every tool spec onto the given `McpServer`. Each handler closure
|
|
66
|
+
* captures the registry so per-call backend resolution stays inside the
|
|
67
|
+
* tool layer (the SDK itself is backend-agnostic).
|
|
68
|
+
*
|
|
69
|
+
* Backend-bound tools are gated on the resolved backend's `tools` set —
|
|
70
|
+
* if the active backend doesn't advertise the tool we throw a
|
|
71
|
+
* `DeviceError("unsupported")` so the SDK returns a structured error to
|
|
72
|
+
* the client. Registry-level tools (`list_available_devices`,
|
|
73
|
+
* `use_device`) skip this gate.
|
|
49
74
|
*/
|
|
50
|
-
export declare function
|
|
75
|
+
export declare function registerAllTools(server: McpServer, registry: BackendRegistry): void;
|