@rebind.gg/mcp-server 0.1.2 → 0.1.4
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 +10 -3
- package/package.json +1 -1
- package/src/coords.ts +7 -0
- package/src/index.ts +39 -21
- package/src/session.ts +12 -0
package/README.md
CHANGED
|
@@ -60,9 +60,16 @@ Off unless set.
|
|
|
60
60
|
cursor and sending relative HID deltas (which map 1:1 to pixels), correcting
|
|
61
61
|
against the true cursor until it lands. This sidesteps the relay's own absolute
|
|
62
62
|
move, whose delta is computed from a cached cursor position that goes stale and
|
|
63
|
-
flings the pointer to a screen edge. A mouse scale probe
|
|
64
|
-
if pointer ballistics are detected (enhance pointer precision,
|
|
65
|
-
probe reports it, since acceleration would break the 1:1
|
|
63
|
+
flings the pointer to a screen edge. A mouse scale probe runs on the first
|
|
64
|
+
click/move_mouse; if pointer ballistics are detected (enhance pointer precision,
|
|
65
|
+
DPI mismatch) the probe reports it, since acceleration would break the 1:1
|
|
66
|
+
assumption.
|
|
67
|
+
|
|
68
|
+
Tool calls are serialized through a single queue: there is one physical cursor
|
|
69
|
+
and keyboard, so parallel tool calls from the model (e.g. two `move_mouse` in
|
|
70
|
+
one turn) run one after another instead of interleaving HID input. All results
|
|
71
|
+
are reported in the coordinate space of the last screenshot, the same space the
|
|
72
|
+
model emits.
|
|
66
73
|
|
|
67
74
|
## Develop
|
|
68
75
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebind.gg/mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MCP server exposing Rebind as computer-use tools: screenshot, verified click, type, key, scroll, and window control over the Remote Access protocol.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/coords.ts
CHANGED
|
@@ -28,6 +28,13 @@ export function mapToScreen(shot: ShotMapping, ix: number, iy: number): Point {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function mapToImage(shot: ShotMapping, sx: number, sy: number): Point {
|
|
32
|
+
return {
|
|
33
|
+
x: Math.round((sx - shot.originX) * shot.scale),
|
|
34
|
+
y: Math.round((sy - shot.originY) * shot.scale),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
export function inImageBounds(
|
|
32
39
|
shot: ShotMapping,
|
|
33
40
|
ix: number,
|
package/src/index.ts
CHANGED
|
@@ -12,8 +12,9 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
12
12
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
13
|
import { RebindRemote } from "@rebind.gg/client-ts";
|
|
14
14
|
import { z } from "zod";
|
|
15
|
+
import { mapToImage } from "./coords.ts";
|
|
15
16
|
import { renderScreenshot } from "./image.ts";
|
|
16
|
-
import { Session } from "./session.ts";
|
|
17
|
+
import { CLICK_EPS_PX, Session } from "./session.ts";
|
|
17
18
|
|
|
18
19
|
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
19
20
|
|
|
@@ -51,13 +52,8 @@ async function main() {
|
|
|
51
52
|
await client.connect();
|
|
52
53
|
|
|
53
54
|
const session = new Session(client);
|
|
54
|
-
try {
|
|
55
|
-
await session.calibrate();
|
|
56
|
-
} catch (e) {
|
|
57
|
-
session.calibration = { calibrated: false, reason: String(e) };
|
|
58
|
-
}
|
|
59
55
|
|
|
60
|
-
const server = new McpServer({ name: "rebind", version: "0.1.
|
|
56
|
+
const server = new McpServer({ name: "rebind", version: "0.1.4" });
|
|
61
57
|
|
|
62
58
|
// REBIND_AGENT_LOG=<path>: opt-in telemetry — one JSONL line per tool call
|
|
63
59
|
// ({ts, tool, ok, latency_ms, note}). No screenshot bytes, no user content.
|
|
@@ -74,17 +70,28 @@ async function main() {
|
|
|
74
70
|
// telemetry must never break a tool call
|
|
75
71
|
}
|
|
76
72
|
};
|
|
73
|
+
// one physical cursor and keyboard: tool calls must never overlap. Models
|
|
74
|
+
// routinely emit parallel calls in a single turn (two move_mouse at once)
|
|
75
|
+
// and the MCP SDK runs handlers concurrently; interleaved HID deltas and
|
|
76
|
+
// cursor readbacks make every overlapping move land nowhere. Chain all
|
|
77
|
+
// handlers through a single queue.
|
|
78
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
77
79
|
const register = (name: string, config: object, handler: (args: any) => Promise<any>) => {
|
|
78
|
-
server.registerTool(name, config as any,
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
server.registerTool(name, config as any, (args: any) => {
|
|
81
|
+
const run = async () => {
|
|
82
|
+
const t0 = performance.now();
|
|
83
|
+
try {
|
|
84
|
+
const result = await handler(args);
|
|
85
|
+
logCall(name, true, t0);
|
|
86
|
+
return result;
|
|
87
|
+
} catch (e) {
|
|
88
|
+
logCall(name, false, t0, String(e));
|
|
89
|
+
throw e;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const p = chain.then(run, run);
|
|
93
|
+
chain = p.catch(() => {});
|
|
94
|
+
return p;
|
|
88
95
|
});
|
|
89
96
|
};
|
|
90
97
|
|
|
@@ -292,10 +299,13 @@ async function main() {
|
|
|
292
299
|
async ({ x, y, button, double }) => {
|
|
293
300
|
const target = session.mapImagePoint(x, y);
|
|
294
301
|
const r = await session.verifiedClick(target.x, target.y, button, double);
|
|
302
|
+
// report in the same image space the model emits — mixing in raw screen
|
|
303
|
+
// coordinates sends smaller models into ad-hoc rescaling math
|
|
304
|
+
const at = mapToImage(session.lastShot!, r.landed.x, r.landed.y);
|
|
295
305
|
return text(
|
|
296
306
|
r.ok
|
|
297
|
-
? `clicked ${button}${double ? " (double)" : ""} at
|
|
298
|
-
: `WARNING: cursor landed at (${
|
|
307
|
+
? `clicked ${button}${double ? " (double)" : ""} at image (${at.x}, ${at.y})${r.corrected ? " after one correction" : ""}`
|
|
308
|
+
: `WARNING: cursor landed at image (${at.x}, ${at.y}) instead of (${x}, ${y}) — the click may have missed. Take a screenshot to verify.`,
|
|
299
309
|
);
|
|
300
310
|
},
|
|
301
311
|
);
|
|
@@ -313,7 +323,14 @@ async function main() {
|
|
|
313
323
|
async ({ x, y }) => {
|
|
314
324
|
const target = session.mapImagePoint(x, y);
|
|
315
325
|
const landed = await session.moveTo(target.x, target.y);
|
|
316
|
-
|
|
326
|
+
const at = mapToImage(session.lastShot!, landed.x, landed.y);
|
|
327
|
+
const missed =
|
|
328
|
+
Math.hypot(landed.x - target.x, landed.y - target.y) > CLICK_EPS_PX;
|
|
329
|
+
return text(
|
|
330
|
+
missed
|
|
331
|
+
? `WARNING: cursor stopped at image (${at.x}, ${at.y}) instead of (${x}, ${y}) — take a fresh screenshot and retry`
|
|
332
|
+
: `cursor at image (${at.x}, ${at.y})`,
|
|
333
|
+
);
|
|
317
334
|
},
|
|
318
335
|
);
|
|
319
336
|
|
|
@@ -413,7 +430,8 @@ async function main() {
|
|
|
413
430
|
"calibrate",
|
|
414
431
|
{
|
|
415
432
|
description:
|
|
416
|
-
"
|
|
433
|
+
"Run the mouse scale probe. Verifies relative mouse counts map 1:1 to pixels; reports pointer-ballistics misconfiguration. " +
|
|
434
|
+
"Auto-runs on first click/move_mouse if not already calibrated; call this tool explicitly to re-run.",
|
|
417
435
|
},
|
|
418
436
|
async () => text(JSON.stringify(await session.calibrate())),
|
|
419
437
|
);
|
package/src/session.ts
CHANGED
|
@@ -54,9 +54,20 @@ export interface CalibrationResult {
|
|
|
54
54
|
export class Session {
|
|
55
55
|
lastShot: ShotMapping | null = null;
|
|
56
56
|
calibration: CalibrationResult | null = null;
|
|
57
|
+
private _calibrating = false;
|
|
57
58
|
|
|
58
59
|
constructor(readonly client: RebindRemote) {}
|
|
59
60
|
|
|
61
|
+
async ensureCalibrated(): Promise<void> {
|
|
62
|
+
if (this.calibration !== null || this._calibrating) return;
|
|
63
|
+
this._calibrating = true;
|
|
64
|
+
try {
|
|
65
|
+
await this.calibrate();
|
|
66
|
+
} finally {
|
|
67
|
+
this._calibrating = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
60
71
|
/** map a point in the last screenshot's image space to screen coords. */
|
|
61
72
|
mapImagePoint(ix: number, iy: number): Point {
|
|
62
73
|
if (!this.lastShot) {
|
|
@@ -83,6 +94,7 @@ export class Session {
|
|
|
83
94
|
* the true cursor. Converges to the exact pixel, usually in a single pass.
|
|
84
95
|
*/
|
|
85
96
|
async moveTo(x: number, y: number): Promise<Point & { passes: number }> {
|
|
97
|
+
await this.ensureCalibrated();
|
|
86
98
|
let at = await this.client.systemMouse();
|
|
87
99
|
let passes = 0;
|
|
88
100
|
while (passes < MOVE_MAX_PASSES) {
|