@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 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 still runs at startup;
64
- if pointer ballistics are detected (enhance pointer precision, DPI mismatch) the
65
- probe reports it, since acceleration would break the 1:1 assumption.
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.2",
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.2" });
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, async (args: any) => {
79
- const t0 = performance.now();
80
- try {
81
- const result = await handler(args);
82
- logCall(name, true, t0);
83
- return result;
84
- } catch (e) {
85
- logCall(name, false, t0, String(e));
86
- throw e;
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 screen (${r.landed.x}, ${r.landed.y})${r.corrected ? " after one correction" : ""}`
298
- : `WARNING: cursor landed at (${r.landed.x}, ${r.landed.y}) instead of (${target.x}, ${target.y}) — the click may have missed. Take a screenshot to verify.`,
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
- return text(`cursor at screen (${landed.x}, ${landed.y})`);
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
- "Re-run the mouse scale probe (auto-runs at startup). Verifies relative mouse counts map 1:1 to pixels; reports pointer-ballistics misconfiguration.",
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) {