granttap-mcp 0.8.9 → 0.8.10

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.
@@ -64,9 +64,12 @@ async function main(): Promise<void> {
64
64
  const helper = installMonitorHelper();
65
65
  let cursorService: InstallResult | null = null;
66
66
  let cursorConfig: InstallResult | null = null;
67
- if (before.cursor.installed) {
67
+ const codexInstalled = before.agents.some((item) => item.agent === "codex" && item.installed);
68
+ if (before.cursor.installed || codexInstalled) {
68
69
  cursorService = installHttpMcpService();
69
- if (cursorService.status !== "manual") cursorConfig = installCursorHttpConfig();
70
+ if (before.cursor.installed && cursorService.status !== "manual") {
71
+ cursorConfig = installCursorHttpConfig();
72
+ }
70
73
  }
71
74
  const pairingResult = await pairIfNeeded();
72
75
  const paired = isMachineConfigured();
@@ -83,7 +86,8 @@ async function main(): Promise<void> {
83
86
  "",
84
87
  `Claude Code ${installed.has("claude") ? state(claudeHook) : "Not installed"}`,
85
88
  `Codex ${installed.has("codex")
86
- ? codexHook.status === "manual" ? "Needs attention" : "Needs hook trust"
89
+ ? codexHook.status === "manual" || cursorService?.status === "manual"
90
+ ? "Needs attention" : !paired ? "Needs connection" : "Authorize in Codex; review hooks"
87
91
  : "Not installed"}`,
88
92
  `Cursor ${before.cursor.installed ? `Beta · ${cursorReady ? "Authorize in Cursor" : "Needs repair"}` : "Not installed"}`,
89
93
  `Grok Build ${installed.has("grok") ? "Ready" : "Not installed"}`,
@@ -92,7 +96,7 @@ async function main(): Promise<void> {
92
96
  `Project Governance ${engine ? "Ready" : "No engine found"}`,
93
97
  "",
94
98
  paired && installed.has("codex")
95
- ? `Next: ${CODEX_TRUST_INSTRUCTION}`
99
+ ? `Next: authorize GrantTap in Codex, then ${CODEX_TRUST_INSTRUCTION}`
96
100
  : paired && before.cursor.installed
97
101
  ? "Next: open Cursor Settings → MCP → GrantTap → Authorize."
98
102
  : !paired
@@ -72,10 +72,10 @@ export async function startHttpMcpServer(options: ServeOptions = {}): Promise<{
72
72
  }
73
73
  const pendingId = String(req.body?.pending_id ?? "");
74
74
  if (!provider.getPending(pendingId)) {
75
- res.status(400).json({ error: "Authorization request expired. Start Authorize again from Cursor Settings." });
75
+ res.status(400).json({ error: "Authorization request expired. Start authorization again from your MCP client." });
76
76
  return;
77
77
  }
78
- if (isMachineConfigured()) {
78
+ if (isMachineConfigured() && req.body?.confirmed !== "true") {
79
79
  res.json({ ok: true, alreadyPaired: true });
80
80
  return;
81
81
  }
@@ -39,10 +39,10 @@ function escapeHtml(value: string): string {
39
39
  .replaceAll('"', "&quot;").replaceAll("'", "&#39;");
40
40
  }
41
41
 
42
- function statusCards(paired: boolean): string {
42
+ function statusCards(paired: boolean, clientName: string): string {
43
43
  const rows = [
44
44
  { id: "phone", label: "iPhone / Apple Watch", status: "action_required", detail: paired ? "Local E2EE keys exist; live phone reachability is not verified on this page." : "Scan the QR or paste the manual token." },
45
- { id: "cursor", label: "Cursor", status: "action_required", detail: "Review this local client and approve access below." },
45
+ { id: "client", label: `${clientName} MCP access`, status: "action_required", detail: "Review this local client and approve access below." },
46
46
  ...integrationRows(),
47
47
  ];
48
48
  return rows.map((row) => {
@@ -51,11 +51,88 @@ function statusCards(paired: boolean): string {
51
51
  }).join("");
52
52
  }
53
53
 
54
+ function reconnectControls(): string {
55
+ return `<div class="client"><p>Already paired with an iPhone? You can approve this client without a new QR.</p>
56
+ <button type="button" id="reconnect">Pair a different iPhone / show a new QR</button>
57
+ <div id="reconnect-confirm" hidden><p>A new QR replaces the current computer pairing. The previously paired iPhone will stop receiving this computer until it scans the new QR.</p>
58
+ <div class="row"><button type="button" id="cancel-reconnect" class="deny">Cancel</button><button type="button" id="confirm-reconnect" class="approve">Replace pairing and show QR</button></div></div></div>`;
59
+ }
60
+
61
+ function consentScript(pendingId: string, paired: boolean): string {
62
+ return `<script>
63
+ const paired = ${paired ? "true" : "false"};
64
+ const status = document.getElementById("status");
65
+ const approve = document.getElementById("approve");
66
+ const qrBox = document.getElementById("qr");
67
+ const manualBox = document.getElementById("manual");
68
+ const manualCode = document.getElementById("manual-code");
69
+ document.getElementById("copy-token").addEventListener("click", async () => {
70
+ try {
71
+ await navigator.clipboard.writeText(manualCode.textContent || "");
72
+ status.textContent = "Manual token copied. It expires after 15 minutes.";
73
+ } catch {
74
+ status.textContent = "Select the manual token and copy it.";
75
+ }
76
+ });
77
+ async function ensurePairing(confirmed = false) {
78
+ if (paired && !confirmed) return;
79
+ const body = new URLSearchParams({ pending_id: ${JSON.stringify(pendingId)} });
80
+ if (confirmed) body.set("confirmed", "true");
81
+ try {
82
+ const response = await fetch("/oauth/pairing", { method: "POST", body });
83
+ const data = await response.json();
84
+ if (!response.ok) {
85
+ status.textContent = data.error || "Pairing failed.";
86
+ return;
87
+ }
88
+ if (data.alreadyPaired) {
89
+ status.textContent = "Local pairing found. Approve this client or check phone reachability with a live request.";
90
+ approve.disabled = false;
91
+ return;
92
+ }
93
+ const image = document.createElement("img");
94
+ image.alt = "GrantTap pairing QR";
95
+ image.src = data.qrDataUrl;
96
+ qrBox.replaceChildren(image);
97
+ qrBox.style.display = "block";
98
+ manualCode.textContent = data.manualToken;
99
+ manualBox.style.display = "block";
100
+ for (const provider of data.providers || []) {
101
+ const chip = document.getElementById("status-" + provider.id);
102
+ if (!chip) continue;
103
+ chip.textContent = provider.status === "connected" ? "Connected" : "Action required";
104
+ chip.className = "chip " + provider.status;
105
+ }
106
+ status.textContent = "Scan with GrantTap on iPhone, then Approve.";
107
+ approve.disabled = false;
108
+ } catch {
109
+ status.textContent = "Could not reach the local GrantTap service. Try again.";
110
+ }
111
+ }
112
+ const reconnect = document.getElementById("reconnect");
113
+ if (reconnect) {
114
+ const confirmation = document.getElementById("reconnect-confirm");
115
+ reconnect.addEventListener("click", () => { confirmation.hidden = false; });
116
+ document.getElementById("cancel-reconnect").addEventListener("click", () => {
117
+ confirmation.hidden = true;
118
+ });
119
+ document.getElementById("confirm-reconnect").addEventListener("click", async () => {
120
+ confirmation.hidden = true;
121
+ reconnect.disabled = true;
122
+ status.textContent = "Creating a new one-time pairing QR…";
123
+ await ensurePairing(true);
124
+ reconnect.disabled = false;
125
+ });
126
+ }
127
+ void ensurePairing();
128
+ </script>`;
129
+ }
130
+
54
131
  export function consentHtml(opts: ConsentOptions): string {
55
132
  const { pendingId, paired, clientName, redirectUri, scopes, resource } = opts;
56
133
  return `<!DOCTYPE html>
57
134
  <html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Authorize GrantTap</title>
58
135
  <style>:root{color-scheme:light dark;font-family:ui-sans-serif,system-ui,sans-serif}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#0f1419;color:#e8eef4}main{width:min(520px,92vw);padding:28px;border-radius:16px;background:#1a222c;box-shadow:0 16px 48px #0008}h1{font-size:1.25rem;margin:0 0 8px}p{margin:0 0 16px;line-height:1.45;color:#b7c4d2;font-size:.95rem}#qr{display:none;margin:0 auto 16px;width:240px;height:240px;background:#fff;border-radius:12px;padding:10px;box-sizing:border-box}#qr img{width:100%;height:100%;object-fit:contain}.client{padding:12px;border:1px solid #334151;border-radius:10px;background:#121920;margin-bottom:16px}.client strong,.client code{display:block;overflow-wrap:anywhere}.client code{margin-top:4px;color:#9fb0c0;font-size:.75rem}.providers{display:grid;gap:8px;margin:0 0 18px}.provider{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:10px 12px;border:1px solid #334151;border-radius:10px}.provider strong,.provider small{display:block}.provider small{margin-top:3px;color:#8fa3b8}.chip{flex:none;padding:4px 7px;border:1px solid;border-radius:999px;font-size:.65rem;font-weight:750;text-transform:uppercase}.connected{color:#68d39c;border-color:#68d39c66}.action_required{color:#f0bb7b;border-color:#f0bb7b66}.not_configured{color:#8fa3b8;border-color:#8fa3b866}#manual{display:none;margin:0 0 16px;padding:12px;border:1px solid #334151;border-radius:10px}#manual code{display:block;margin:7px 0;padding:8px;overflow-wrap:anywhere;background:#0c1117;border-radius:7px;user-select:all}.row{display:flex;gap:10px}button{flex:1;border:0;border-radius:10px;padding:12px 14px;font-weight:600;cursor:pointer}.approve{background:#3d8bfd;color:#fff}.deny{background:#2a3440;color:#d7e0ea}.status{font-size:.85rem;color:#8fa3b8;min-height:1.2em}</style></head>
59
- <body><main><h1>Authorize ${escapeHtml(clientName)} → GrantTap</h1><p>Grant this local MCP client access to GrantTap tools on this Mac. E2EE keys stay local in <code>~/.granttap</code>.</p><div class="client"><strong>Requesting client: ${escapeHtml(clientName)}</strong><code>Permission: ${escapeHtml(scopes.join(" "))}</code><code>Resource: ${escapeHtml(resource)}</code><code>Redirect: ${escapeHtml(redirectUri)}</code></div><div class="providers">${statusCards(paired)}</div><div id="qr"></div><div id="manual"><strong>Camera unavailable?</strong><span> Paste this one-time token in the GrantTap app:</span><code id="manual-code"></code><button type="button" id="copy-token">Copy token</button></div><p class="status" id="status">${paired ? "Local pairing keys found; phone reachability is not verified here." : "Creating a pairing QR…"}</p><p>Approve grants this MCP client local tool access. It does not prove that the phone has scanned the pairing code.</p><form id="form" method="POST" action="/consent" class="row"><input type="hidden" name="pending_id" value="${pendingId}" /><button class="deny" type="submit" name="decision" value="deny">Deny</button><button class="approve" type="submit" name="decision" value="approve" id="approve" ${paired ? "" : "disabled"}>Approve</button></form></main>
60
- <script>const paired=${paired ? "true" : "false"};const status=document.getElementById("status");const approve=document.getElementById("approve");const qrBox=document.getElementById("qr");const manualBox=document.getElementById("manual");const manualCode=document.getElementById("manual-code");const copyToken=document.getElementById("copy-token");copyToken.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(manualCode.textContent||"");status.textContent="Manual token copied. It expires after 15 minutes."}catch{status.textContent="Select the manual token and copy it."}});async function ensurePairing(){if(paired)return;const body=new URLSearchParams({pending_id:${JSON.stringify(pendingId)}});const res=await fetch("/oauth/pairing",{method:"POST",body});const data=await res.json();if(!res.ok){status.textContent=data.error||"Pairing failed";return}if(data.alreadyPaired){status.textContent="Local pairing keys found. Review the client and Approve; verify the phone with a live request.";approve.disabled=false;return}qrBox.style.display="block";qrBox.innerHTML='<img alt="GrantTap pairing QR" src="'+data.qrDataUrl+'" />';manualCode.textContent=data.manualToken;manualBox.style.display="block";for(const provider of data.providers||[]){const chip=document.getElementById("status-"+provider.id);if(!chip)continue;chip.textContent=provider.status==="connected"?"Connected":"Action required";chip.className="chip "+provider.status}status.textContent="Scan with GrantTap on iPhone, then Approve.";approve.disabled=false}ensurePairing().catch((err)=>{status.textContent=String(err)});</script></body></html>`;
136
+ <body><main><h1>Authorize ${escapeHtml(clientName)} → GrantTap</h1><p>Grant this local MCP client access to GrantTap tools on this Mac. E2EE keys stay local in <code>~/.granttap</code>.</p><div class="client"><strong>Requesting client: ${escapeHtml(clientName)}</strong><code>Permission: ${escapeHtml(scopes.join(" "))}</code><code>Resource: ${escapeHtml(resource)}</code><code>Redirect: ${escapeHtml(redirectUri)}</code></div><div class="providers">${statusCards(paired, clientName)}</div>${paired ? reconnectControls() : ""}<div id="qr"></div><div id="manual"><strong>Camera unavailable?</strong><span> Paste this one-time token in the GrantTap app:</span><code id="manual-code"></code><button type="button" id="copy-token">Copy token</button></div><p class="status" id="status">${paired ? "Local pairing keys found; phone reachability is not verified here." : "Creating a pairing QR…"}</p><p>Approve grants this MCP client local tool access. It does not prove that the phone has scanned the pairing code.</p><form id="form" method="POST" action="/consent" class="row"><input type="hidden" name="pending_id" value="${pendingId}" /><button class="deny" type="submit" name="decision" value="deny">Deny</button><button class="approve" type="submit" name="decision" value="approve" id="approve" ${paired ? "" : "disabled"}>Approve</button></form></main>
137
+ ${consentScript(pendingId, paired)}</body></html>`;
61
138
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Loopback OAuth 2.1 provider for Cursor Settings → Authorize.
2
+ * Loopback OAuth 2.1 provider for Codex and Cursor MCP authorization.
3
3
  *
4
- * Authorize means: confirm this Mac's GrantTap pairing for Cursor (issue a
4
+ * Authorize means: confirm this Mac's GrantTap pairing for a client (issue a
5
5
  * bearer token). E2EE keys stay in ~/.granttap — OAuth does not replace pair.
6
6
  */
7
7
  import { randomUUID } from "node:crypto";
@@ -104,10 +104,10 @@ export class GrantTapOAuthProvider implements OAuthServerProvider {
104
104
  }));
105
105
  }
106
106
 
107
- /** Complete consent: issue code and redirect to Cursor. */
107
+ /** Complete consent: issue code and redirect to the requesting MCP client. */
108
108
  completeConsent(pendingId: string, approve: boolean): { redirectUrl: string } {
109
109
  const pending = this.getPending(pendingId);
110
- if (!pending) throw new Error("Authorization request expired. Start Authorize again from Cursor Settings.");
110
+ if (!pending) throw new Error("Authorization request expired. Start authorization again from your MCP client.");
111
111
 
112
112
  const target = new URL(pending.params.redirectUri);
113
113
  if (!approve) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granttap-mcp",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
4
4
  "description": "Personal live control center runtime for local coding agents on iPhone and Apple Watch.",
5
5
  "type": "module",
6
6
  "bin": {