granttap-mcp 0.8.34 → 0.8.35

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
@@ -4,16 +4,18 @@
4
4
  [![CI](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/sergii-ziborov/granttap-mcp/actions/workflows/ci.yml)
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
 
7
- GrantTap is a distributed Project control system for local coding agents.
7
+ > **See what your coding agents are doing. Step in when they need you.**
8
8
 
9
- > See what your coding agents are doing. Step in when they need you.
9
+ GrantTap connects local Claude Code and Codex work to a live control center on
10
+ Mac, iPhone, iPad, and Apple Watch. See the Task and its latest activity, answer
11
+ an approval or question with context, and continue the same goal as sessions or
12
+ computers change. Cursor and Grok Build join the view where their local
13
+ integrations support it.
10
14
 
11
- This repository is the canonical machine runtime: CLI, MCP server, provider
12
- hooks, local adapters, and TypeScript wire schemas. It connects the Mac,
13
- iPhone, iPad, and Apple Watch product surfaces to local agents, Project Mesh,
14
- Governance, auto-accept, and Engine-backed evidence. Agents and provider
15
- credentials stay on your computer. Native controller traffic is end-to-end
16
- encrypted.
15
+ This repository supplies the local runtime: CLI, MCP server, provider hooks,
16
+ adapters, and wire contracts. It powers Project Mesh, Governance, and observed
17
+ usage while agents and provider credentials stay on your computer. Controller
18
+ traffic is end-to-end encrypted.
17
19
 
18
20
  The MCP runtime, plugins, and CLI are open source under the MIT License.
19
21
  The Apple apps, Engine, relay, and website are separately licensed commercial
@@ -36,11 +38,13 @@ products or source. Their licenses do not change this repository's MIT grant.
36
38
  <img src="docs/images/apple-watch-approval.png" width="180" alt="GrantTap approval on Apple Watch">
37
39
  </p>
38
40
 
41
+ <p align="center"><em>GrantTap iPhone and Apple Watch screens with deterministic sample work; no private user tasks are shown.</em></p>
42
+
39
43
  ## Supported providers
40
44
 
41
- - Primary: Claude Code and Codex.
42
- - Beta: Cursor.
43
- - Experimental where available: Grok Build.
45
+ - Claude Code and Codex: primary local control and continuation paths.
46
+ - Cursor: Task visibility and supported local controls, with narrower coverage.
47
+ - Grok Build: observable work where the installed runtime exposes it.
44
48
 
45
49
  GrantTap reports the depth each provider actually exposes. Visibility does not
46
50
  imply deterministic remote blocking or full mobile continuation.
@@ -147,7 +151,7 @@ computers. GrantTap builds a bounded Task Capsule from explicit task/git facts,
147
151
  requires local phone authorization, creates a separate target branch/worktree,
148
152
  starts the target execution, and returns a receipt bound to the exact capsule.
149
153
  Cursor uses the same provider-neutral schema and trusted caller attribution.
150
- Grok Build remains Experimental and observable where available, but does not
154
+ Grok Build remains observable where available, but does not
151
155
  yet expose a trusted caller hook, so agent-authored scoped Mesh events are not
152
156
  offered for it. Unsupported remote-start paths fail closed instead of claiming
153
157
  parity.
@@ -23,11 +23,12 @@ checks parent messages, refreshes pending pairing while visible, and keeps
23
23
  copyable transfer material out of model-visible text and persistent UI storage.
24
24
  Non-UI clients can still use the tools and display the returned QR image.
25
25
 
26
- The connection card itself does not log in to an account. The Codex-owned
27
- plugin management page is not this UI. The optional Mac passkey choice lives
28
- in the browser authorization page for the loopback HTTP MCP server. It grants
29
- the coding app an MCP token, without creating a phone pairing or changing the
30
- local provider runtime.
26
+ Connect with passkey on the card creates a short-lived, authenticated website
27
+ request. A fresh passkey assertion on granttap.com links this Mac to the user's
28
+ account; the site returns a machine token only to the local MCP watcher, which
29
+ saves it privately. The QR pairing room and provider runtime remain unchanged.
30
+ The separate coding-app OAuth page can also use passkey to authorize that MCP
31
+ client. The Codex-owned plugin management page is not this card.
31
32
 
32
33
  Tests in `tests/` exercise isolated keys, a loopback encrypted phone connection,
33
34
  expiry, non-mutating status, and actual DOM button/host-message behavior.
@@ -72,6 +72,7 @@ async function call(name, args = {}) {
72
72
  : await request("tools/call", { name, arguments: args });
73
73
  if (result?.isError) message("Could not complete the request. Refresh status and check the relay before retrying.");
74
74
  else { render(result); message("Status updated."); }
75
+ return result;
75
76
  } catch {
76
77
  message("No successful response. Refresh status before retrying; pairing may already have changed.");
77
78
  } finally {
@@ -0,0 +1,27 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { buildConnectSnapshot } from "../oauth/session/connect-snapshot";
3
+ import { publishConnectRequestRetry, watchConnectDecision, websiteOrigin } from "../oauth/consent/website-session";
4
+
5
+ const REQUEST_LIFETIME_MS = 15 * 60_000;
6
+ let pending: { url: string; expiresAt: number } | null = null;
7
+
8
+ /** Start a browser passkey ceremony for this Mac. No account credential enters MCP output. */
9
+ export async function startPasskeyAccountLink(): Promise<string> {
10
+ if (pending && pending.expiresAt > Date.now()) return pending.url;
11
+ const origin = websiteOrigin();
12
+ if (!origin) throw new Error("GrantTap account service is unavailable.");
13
+ const id = randomUUID();
14
+ const secret = randomBytes(32).toString("base64url");
15
+ const snapshot = () => ({ ...buildConnectSnapshot("GrantTap MCP"), purpose: "account-link" as const });
16
+ if (!await publishConnectRequestRetry(origin, id, snapshot(), 3, secret)) {
17
+ throw new Error("GrantTap account service did not accept the connection request.");
18
+ }
19
+ watchConnectDecision(origin, id, snapshot, (approved, method) => {
20
+ if (!approved || method !== "passkey") throw new Error("Use the account passkey to link this Mac.");
21
+ pending = null;
22
+ return { redirectUrl: `${origin}/account` };
23
+ }, { requestSecret: secret });
24
+ const url = `${origin}/connect#request=${id}`;
25
+ pending = { url, expiresAt: Date.now() + REQUEST_LIFETIME_MS };
26
+ return url;
27
+ }
@@ -6,6 +6,7 @@ import { inspectProviderStatusSnapshot } from "../status/provider-status";
6
6
  import { packageVersion } from "../status/package-version";
7
7
  import { connectionRuntimeStatus } from "../mcp-tools/connect/relay";
8
8
  import { beginEnrollment, cancelEnrollment, enrollmentIsOpen } from "./enrollment";
9
+ import { loadAccountLink } from "../../../bridge/src/account-recovery/link";
9
10
 
10
11
  export const connectionOutput = {
11
12
  status: z.enum(["disconnected", "paired", "pairing", "expired", "connected"]),
@@ -23,6 +24,7 @@ export const connectionOutput = {
23
24
  expiresInMinutes: z.number().int().positive().nullable(),
24
25
  providers: z.array(z.object({ id: z.string(), status: z.string(), detail: z.string() })),
25
26
  roomPrefix: z.string(),
27
+ accountLinkSaved: z.boolean(),
26
28
  };
27
29
 
28
30
  export type PendingCode = {
@@ -76,6 +78,7 @@ export class ConnectionState {
76
78
  expiresInMinutes: code ? Math.max(1, Math.ceil((code.expiresAt - now) / 60_000)) : null,
77
79
  providers: inspectProviderStatusSnapshot().providers.map(({ id, status, detail }) => ({ id, status, detail })),
78
80
  roomPrefix: config?.room ? config.room.slice(0, 8) : "",
81
+ accountLinkSaved: loadAccountLink() !== null,
79
82
  },
80
83
  _meta: { granttap: code ? { pairingUri: code.pairingUri, qrDataUrl: code.qrDataUrl } : {} },
81
84
  };
@@ -21,6 +21,16 @@ function render(value) {
21
21
  current = state;
22
22
  clearCode();
23
23
  const meta = result._meta?.granttap ?? {};
24
+ const passkeyUrl = typeof meta.passkeyUrl === "string" && /^https:\/\/granttap\.com\/connect#request=[0-9a-f-]{36}$/i.test(meta.passkeyUrl)
25
+ ? meta.passkeyUrl : "";
26
+ $("passkey-link").classList.toggle("hidden", !passkeyUrl || state.accountLinkSaved === true);
27
+ if (passkeyUrl) $("passkey-link").setAttribute("href", passkeyUrl);
28
+ else $("passkey-link").removeAttribute("href");
29
+ $("account-status").textContent = state.accountLinkSaved === true
30
+ ? "Saved account link on this Mac. Current server access is checked when it is used."
31
+ : "No account linked on this Mac. A passkey can link it without changing the QR pairing.";
32
+ $("passkey").textContent = state.accountLinkSaved === true
33
+ ? "Reauthorize with passkey" : "Connect with passkey";
24
34
  const labels = {
25
35
  disconnected: ["Not paired", "Add a device", "Tap Add a device. Scan the QR in GrantTap on iPhone, iPad, or Android."],
26
36
  pairing: ["Waiting for a device", "Scan this QR", "Keep this card open. The QR is only here — not in chat."],
@@ -91,6 +101,7 @@ $("connect").addEventListener("click", () => {
91
101
  call("connect");
92
102
  });
93
103
  $("refresh").addEventListener("click", () => call("connection_status"));
104
+ $("passkey").addEventListener("click", () => call("connect_with_passkey"));
94
105
  $("reconnect").addEventListener("click", () => {
95
106
  pendingMode = "reconnect";
96
107
  $("confirm-copy").textContent = "Reconnect shows a new QR for the existing controller device in this room.";
@@ -27,7 +27,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
27
27
  </svg>
28
28
  </div><div><div class="title">GrantTap</div><div class="status"><span class="dot"></span><span id="status">Checking connection…</span></div></div></header>
29
29
  <section class="body">
30
- <div class="copy"><h2 id="heading">Add a device</h2><p id="detail">Scan the QR in GrantTap on iPhone, iPad, or Android. It appears here as soon as pairing starts.</p><ul class="phones" id="phones"></ul><p class="empty" id="phones-empty">No saved pairing on this Mac yet.</p><div class="actions"><button class="primary" id="connect">Add a device</button><button id="reconnect">Reconnect</button><button id="new-qr" class="hidden">Create a new QR</button><button id="refresh">Refresh status</button><button id="copy" class="hidden">Copy pairing link</button></div><div class="message small" id="message" role="status"></div></div>
30
+ <div class="copy"><h2 id="heading">Add a device</h2><p id="detail">Scan the QR in GrantTap on iPhone, iPad, or Android. It appears here as soon as pairing starts.</p><ul class="phones" id="phones"></ul><p class="empty" id="phones-empty">No saved pairing on this Mac yet.</p><div class="actions"><button class="primary" id="connect">Add a device</button><button id="reconnect">Reconnect</button><button id="new-qr" class="hidden">Create a new QR</button><button id="refresh">Refresh status</button><button id="copy" class="hidden">Copy pairing link</button></div><p id="account-status" class="small">Checking account link…</p><div class="actions"><button id="passkey">Connect with passkey</button><a id="passkey-link" class="hidden" target="_blank" rel="noopener noreferrer">Continue on granttap.com</a></div><div class="message small" id="message" role="status"></div></div>
31
31
  <img id="qr" class="hidden" alt="GrantTap pairing QR" />
32
32
  </section>
33
33
  <p id="expiry" class="small" aria-live="polite"></p>
@@ -36,9 +36,8 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
36
36
  <dt>Relay observed by this MCP</dt><dd id="relay">Not checked</dd>
37
37
  <dt>Last encrypted message from iPhone</dt><dd id="phone">Not observed</dd></dl>
38
38
  <h3>Provider readiness</h3><ul id="providers"></ul>
39
- <p>No GrantTap account or password is required. Pair using GrantTap on iPhone → Settings → Connections.
40
- Provider sign-in is separate.</p>
41
- <p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.23</code> and <code>granttap setup</code> locally.
39
+ <p>QR pairs a phone directly. A passkey links this Mac to your GrantTap account so your signed-in phone can discover its computers. Provider sign-in is separate.</p>
40
+ <p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.35</code> and <code>granttap setup</code> locally.
42
41
  A saved pairing alone does not prove your phone or the background helper is online.</p>
43
42
  <p><a href="https://granttap.com" target="_blank" rel="noopener noreferrer">Website</a> ·
44
43
  <a href="https://granttap.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> ·
@@ -7,6 +7,7 @@ import { isMachineConfigured } from "../../status/pairing-status";
7
7
  import { ConnectionState, connectionOutput } from "../../connection-center/state";
8
8
  import { CONNECTION_WIDGET_URI } from "./widget";
9
9
  import { resetRelay, relay } from "./relay";
10
+ import { startPasskeyAccountLink } from "../../connection-center/passkey";
10
11
 
11
12
  const widgetMeta = {
12
13
  ui: { resourceUri: CONNECTION_WIDGET_URI, visibility: ["model", "app"] },
@@ -39,6 +40,24 @@ export function registerConnectTool(server: McpServer): void {
39
40
  annotations: { ...changes, readOnlyHint: true, idempotentHint: true },
40
41
  _meta: widgetMeta,
41
42
  }, async () => connectionResult(state));
43
+ server.registerTool("connect_with_passkey", {
44
+ title: "Link this Mac to a GrantTap account",
45
+ description: "Open a short-lived GrantTap account passkey request for this Mac. The passkey is entered only on granttap.com; existing QR pairings remain unchanged.",
46
+ inputSchema: {},
47
+ outputSchema: connectionOutput,
48
+ annotations: changes,
49
+ _meta: widgetMeta,
50
+ }, async () => {
51
+ try {
52
+ const passkeyUrl = await startPasskeyAccountLink();
53
+ const result = connectionResult(state);
54
+ const meta = result._meta?.granttap;
55
+ const granttap = meta && typeof meta === "object" ? meta : {};
56
+ return { ...result, _meta: { granttap: { ...granttap, passkeyUrl } } };
57
+ } catch {
58
+ return { isError: true, content: [{ type: "text", text: "GrantTap could not start passkey linking. Check the account service and try again." }] };
59
+ }
60
+ });
42
61
  server.registerTool(
43
62
  "connect",
44
63
  {
@@ -1,6 +1,6 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
 
3
- export const CONNECTION_WIDGET_URI = "ui://granttap/connection/v3.html";
3
+ export const CONNECTION_WIDGET_URI = "ui://granttap/connection/v4.html";
4
4
 
5
5
  import { readFileSync } from "node:fs";
6
6
 
@@ -13,7 +13,7 @@ export function registerConnectionWidget(server: McpServer): void {
13
13
  CONNECTION_WIDGET_URI,
14
14
  {
15
15
  title: "GrantTap connection center",
16
- description: "GrantTap card: devices in this pairing room, Add a device, and the QR on this card only.",
16
+ description: "GrantTap card: QR device pairing, Mac account passkey linking, and connection status.",
17
17
  mimeType: "text/html;profile=mcp-app",
18
18
  },
19
19
  async (uri) => ({
@@ -23,7 +23,7 @@ export function registerConnectionWidget(server: McpServer): void {
23
23
  text: html,
24
24
  _meta: {
25
25
  ui: { prefersBorder: true, permissions: { clipboardWrite: {} }, csp: { connectDomains: [], resourceDomains: [] } },
26
- "openai/widgetDescription": "List devices in this pairing room and add one with a QR that stays on this card.",
26
+ "openai/widgetDescription": "Pair a device by QR or link this Mac to your account with passkey.",
27
27
  "openai/widgetPrefersBorder": true,
28
28
  },
29
29
  }],
@@ -10,7 +10,7 @@ keeps each phone response tied to the exact originating chat and prompt.
10
10
  Install the supported CLI on this computer, then run:
11
11
 
12
12
  ```bash
13
- npm install -g granttap-mcp@0.8.34
13
+ npm install -g granttap-mcp@0.8.35
14
14
  granttap setup
15
15
  granttap status
16
16
  ```
@@ -4,7 +4,7 @@
4
4
  "command": "node",
5
5
  "args": [
6
6
  "-e",
7
- "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.34\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
7
+ "\"use strict\";\n\n// Cursor may start this MCP from any cwd. Execute JavaScript entry points with\n// Node itself: Windows batch shims require shell quoting that differs by host.\nconst { spawn } = require(\"node:child_process\");\nconst { existsSync } = require(\"node:fs\");\nconst { delimiter, dirname, join } = require(\"node:path\");\n\nconst PACKAGE = \"granttap-mcp@0.8.35\";\nconst dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === \"1\";\nconst platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM\n ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;\nconst nodeRoot = dryRun && process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT\n ? process.env.GRANTTAP_BOOTSTRAP_NODE_ROOT : dirname(process.execPath);\nconst pathRoots = (process.env.PATH || \"\").split(delimiter).filter(Boolean);\nconst appData = process.env.APPDATA;\nconst roots = [nodeRoot, ...pathRoots, ...(appData ? [join(appData, \"npm\")] : [])];\nconst first = (paths) => paths.find((path) => existsSync(path));\n\nfunction launch() {\n if (platform !== \"win32\") {\n const installed = first(roots.map((root) => join(root, \"granttap-mcp\")));\n if (installed) return { command: installed, args: [] };\n const npx = first(roots.map((root) => join(root, \"npx\"))) || \"npx\";\n return { command: npx, args: [\"-y\", PACKAGE] };\n }\n const installed = first(roots.map((root) =>\n join(root, \"node_modules\", \"granttap-mcp\", \"bin\", \"granttap-mcp.mjs\")));\n if (installed) return { command: process.execPath, args: [installed] };\n const npx = first(roots.map((root) =>\n join(root, \"node_modules\", \"npm\", \"bin\", \"npx-cli.js\")));\n if (npx) return { command: process.execPath, args: [npx, \"-y\", PACKAGE] };\n throw new Error(\"Node.js/npm 20+ is required on this Windows device. Install npm and retry GrantTap.\");\n}\n\nlet target;\ntry { target = launch(); }\ncatch (error) {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n}\n\nif (dryRun) {\n process.stdout.write(JSON.stringify(target));\n process.exit(0);\n}\n\nconst child = spawn(target.command, target.args, { stdio: \"inherit\", windowsHide: true });\nchild.on(\"error\", (error) => {\n process.stderr.write(`[granttap] ${error.message}\\n`);\n process.exit(1);\n});\nchild.on(\"exit\", (code, signal) => {\n if (signal && platform !== \"win32\") {\n try { process.kill(process.pid, signal); } catch { process.exit(code ?? 1); }\n return;\n }\n process.exit(code ?? 1);\n});\n"
8
8
  ]
9
9
  }
10
10
  }
@@ -7,7 +7,7 @@ const { spawn } = require("node:child_process");
7
7
  const { existsSync } = require("node:fs");
8
8
  const { delimiter, dirname, join } = require("node:path");
9
9
 
10
- const PACKAGE = "granttap-mcp@0.8.34";
10
+ const PACKAGE = "granttap-mcp@0.8.35";
11
11
  const dryRun = process.env.GRANTTAP_BOOTSTRAP_DRY_RUN === "1";
12
12
  const platform = dryRun && process.env.GRANTTAP_BOOTSTRAP_PLATFORM
13
13
  ? process.env.GRANTTAP_BOOTSTRAP_PLATFORM : process.platform;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.34",
3
+ "version": "0.8.35",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {