granttap-mcp 0.6.4 → 0.6.7
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 +103 -10
- package/SECURITY.md +13 -3
- package/apps/bridge/src/adapters.ts +50 -0
- package/apps/bridge/src/approval-state.ts +715 -0
- package/apps/bridge/src/approval.ts +219 -15
- package/apps/bridge/src/bin/claude-hook.ts +64 -15
- package/apps/bridge/src/bin/codex-hook.ts +44 -6
- package/apps/bridge/src/bin/codex-policy-hook.ts +38 -0
- package/apps/bridge/src/bin/connect.ts +8 -3
- package/apps/bridge/src/bin/cursor-after-shell.ts +97 -0
- package/apps/bridge/src/bin/cursor-hook.ts +96 -0
- package/apps/bridge/src/bin/cursor-mcp-hook.ts +103 -0
- package/apps/bridge/src/bin/pair-uri.ts +28 -0
- package/apps/bridge/src/bin/setup.ts +16 -2
- package/apps/bridge/src/bin/web.ts +33 -0
- package/apps/bridge/src/capabilities.ts +43 -8
- package/apps/bridge/src/cloud-approvals.ts +324 -0
- package/apps/bridge/src/config.ts +347 -14
- package/apps/bridge/src/cursor-mcp-policy.ts +163 -0
- package/apps/bridge/src/delivery.ts +61 -13
- package/apps/bridge/src/install.ts +491 -66
- package/apps/bridge/src/monitor.ts +229 -60
- package/apps/bridge/src/pair-uri-file.ts +46 -0
- package/apps/bridge/src/pairing.ts +9 -1
- package/apps/bridge/src/policy.ts +160 -0
- package/apps/bridge/src/scheduler.ts +3 -1
- package/apps/bridge/src/session-keys.ts +2 -1
- package/apps/bridge/src/sessions/activity-helpers.ts +149 -0
- package/apps/bridge/src/sessions/child-threads.ts +77 -0
- package/apps/bridge/src/sessions/claude.ts +549 -0
- package/apps/bridge/src/sessions/codex.ts +762 -0
- package/apps/bridge/src/sessions/common.ts +123 -0
- package/apps/bridge/src/sessions/copilot.ts +411 -0
- package/apps/bridge/src/sessions/cursor.ts +868 -0
- package/apps/bridge/src/sessions/telemetry.ts +562 -0
- package/apps/bridge/src/sessions.ts +184 -613
- package/apps/mcp/src/bin/authorize.ts +90 -0
- package/apps/mcp/src/bin/serve.ts +31 -0
- package/apps/mcp/src/bin/status.ts +39 -0
- package/apps/mcp/src/create-server.ts +344 -0
- package/apps/mcp/src/cursor-config.ts +148 -0
- package/apps/mcp/src/http-server.ts +234 -0
- package/apps/mcp/src/http-service.ts +396 -0
- package/apps/mcp/src/oauth-provider.ts +467 -0
- package/apps/mcp/src/pairing-status.ts +25 -0
- package/apps/mcp/src/provider-status.ts +239 -0
- package/apps/mcp/src/server.ts +6 -225
- package/bin/granttap-mcp.mjs +83 -16
- package/cursor-plugin/.cursor-plugin/marketplace.json +16 -0
- package/cursor-plugin/.cursor-plugin/plugin.json +29 -0
- package/cursor-plugin/README.md +73 -0
- package/cursor-plugin/assets/logo.svg +7 -0
- package/cursor-plugin/commands/connect.md +19 -0
- package/cursor-plugin/mcp.json +8 -0
- package/cursor-plugin/rules/dual-channel.mdc +11 -0
- package/cursor-plugin/skills/connect/SKILL.md +89 -0
- package/docs/cursor-authorize.md +67 -0
- package/docs/images/apple-watch-approval.png +0 -0
- package/docs/images/apple-watch-inbox.png +0 -0
- package/docs/images/apple-watch-task.png +0 -0
- package/docs/images/iphone-claude-tasks.png +0 -0
- package/docs/images/iphone-command-center.png +0 -0
- package/docs/images/iphone-mcp-usage.png +0 -0
- package/docs/images/iphone-task-detail.png +0 -0
- package/docs/images/phone-activity.png +0 -0
- package/docs/images/watch-approval.png +0 -0
- package/package.json +11 -3
- package/packages/core/relay-client.ts +74 -10
- package/packages/protocol/schema.ts +186 -12
package/README.md
CHANGED
|
@@ -75,16 +75,23 @@ not separate MCP billing.
|
|
|
75
75
|
Add GrantTap to each agent you use:
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
npm install
|
|
79
|
+
npm install -g .
|
|
80
|
+
codex mcp add granttap -- granttap
|
|
81
|
+
claude mcp add granttap -- granttap
|
|
80
82
|
```
|
|
81
83
|
|
|
84
|
+
Those commands use this checkout. Publish/release it before replacing the local
|
|
85
|
+
install with an npm registry version.
|
|
86
|
+
|
|
82
87
|
Start a fresh agent task and say **“Connect GrantTap.”** The `connect` tool:
|
|
83
88
|
|
|
84
89
|
1. creates a new end-to-end encrypted pairing;
|
|
85
|
-
2. returns a scannable one-time QR directly in the agent chat
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
2. returns a scannable one-time QR directly in the agent chat.
|
|
91
|
+
|
|
92
|
+
Then run the separate MCP `setup` tool. It installs Cursor shell/MCP policy
|
|
93
|
+
hooks, the complete Claude Code matcher, both Codex hooks, and the per-user
|
|
94
|
+
background helper for task sync and schedules.
|
|
88
95
|
|
|
89
96
|
Scan the QR with GrantTap on iPhone. No terminal QR, copied pairing JSON, or
|
|
90
97
|
open background terminal is required.
|
|
@@ -93,8 +100,9 @@ If an MCP client cannot render image content, use the CLI fallback:
|
|
|
93
100
|
|
|
94
101
|
```bash
|
|
95
102
|
npm install -g granttap-mcp
|
|
96
|
-
granttap
|
|
97
|
-
granttap
|
|
103
|
+
granttap connect
|
|
104
|
+
granttap setup
|
|
105
|
+
granttap status
|
|
98
106
|
```
|
|
99
107
|
|
|
100
108
|
CLI `connect` prints a one-time QR and short manual code. `setup` is idempotent:
|
|
@@ -102,6 +110,72 @@ it preserves unrelated agent settings and backs up a configuration file before
|
|
|
102
110
|
changing it. The pairing is stored locally in `~/.granttap/machine.json`.
|
|
103
111
|
Existing beta state under `~/.nodvox/` is migrated automatically.
|
|
104
112
|
|
|
113
|
+
After setup, open `/hooks` in Codex, review and trust both exact GrantTap hooks,
|
|
114
|
+
then restart Codex. Installation alone is not reported as trusted or connected.
|
|
115
|
+
|
|
116
|
+
## Cursor Settings → Authorize
|
|
117
|
+
|
|
118
|
+
Cursor only shows **Authorize / Sign in** for **HTTP/SSE** MCP servers that
|
|
119
|
+
speak OAuth (same pattern as Lovable / Figma). The default stdio entry
|
|
120
|
+
(`command` + `args`) cannot show that button — Cursor documents stdio auth as
|
|
121
|
+
**Manual**.
|
|
122
|
+
|
|
123
|
+
To enable Authorize for GrantTap, use the one-step local setup:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install -g granttap-mcp
|
|
127
|
+
granttap authorize
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
It preserves unrelated entries in `~/.cursor/mcp.json`, replaces only the
|
|
131
|
+
`granttap` entry with the loopback HTTP endpoint, keeps a one-time
|
|
132
|
+
`.bak-granttap`, and starts the OAuth MCP server. Then open
|
|
133
|
+
**Cursor Settings → MCP → GrantTap → Authorize**. The local consent page shows
|
|
134
|
+
the pairing QR plus a manual-token fallback when the Mac is not paired yet.
|
|
135
|
+
`granttap authorize` installs a loopback-only per-user LaunchAgent, verifies its
|
|
136
|
+
exact `/healthz` identity, writes Cursor config only after that check succeeds,
|
|
137
|
+
and exits. The service uses RunAtLoad + KeepAlive, so closing the terminal or
|
|
138
|
+
restarting the Mac does not leave Cursor pointing at a dead URL.
|
|
139
|
+
|
|
140
|
+
The foreground troubleshooting flow is:
|
|
141
|
+
|
|
142
|
+
1. Run the local HTTP OAuth server (loopback only):
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
granttap serve
|
|
146
|
+
# listens on http://127.0.0.1:17342/mcp
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
2. Point Cursor at the HTTP URL in `~/.cursor/mcp.json` (replace the stdio
|
|
150
|
+
`command` entry):
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"mcpServers": {
|
|
155
|
+
"granttap": {
|
|
156
|
+
"url": "http://127.0.0.1:17342/mcp"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
3. Open **Cursor Settings → MCP → GrantTap** and click **Authorize**.
|
|
163
|
+
A local browser page confirms linking Cursor to this Mac’s pairing
|
|
164
|
+
(`~/.granttap`). If unpaired, it shows a one-time QR first.
|
|
165
|
+
|
|
166
|
+
OAuth tokens are stored in `~/.granttap/mcp-oauth.json` (mode `0600`). They do
|
|
167
|
+
**not** replace E2EE pairing keys. Claude Code / Codex can keep using the
|
|
168
|
+
stdio transport; Authorize is a Cursor Settings affordance.
|
|
169
|
+
|
|
170
|
+
Details: [docs/cursor-authorize.md](docs/cursor-authorize.md).
|
|
171
|
+
|
|
172
|
+
`granttap setup` is the policy-hook entry point: it installs Cursor
|
|
173
|
+
`beforeShellExecution`, `afterShellExecution`, and `beforeMCPExecution` hooks,
|
|
174
|
+
plus Claude/Codex hooks and background task sync. It does not opt a new user
|
|
175
|
+
into OAuth; when an exact Cursor HTTP entry already exists, it also repairs the
|
|
176
|
+
persistent OAuth service. Per-chat blocks are checked before phone routing,
|
|
177
|
+
and ambiguous/unscoped Cursor calls fall back to Cursor's native permission UI.
|
|
178
|
+
|
|
105
179
|
## Codex and Claude Code: honest capability matrix
|
|
106
180
|
|
|
107
181
|
| Capability | Codex | Claude Code |
|
|
@@ -109,7 +183,7 @@ Existing beta state under `~/.nodvox/` is migrated automatically.
|
|
|
109
183
|
| Approval hook | `PermissionRequest` when Codex hooks are enabled | `PreToolUse` |
|
|
110
184
|
| Resume an existing task | Yes, through the local Codex CLI | Yes, through the local Claude CLI |
|
|
111
185
|
| Start a new persistent task | Yes | Yes |
|
|
112
|
-
| Up to five attachments | Images through Codex image inputs; documents as local paths | Local image/document paths in the turn |
|
|
186
|
+
| Up to five attachments within one encrypted-frame budget | Images through Codex image inputs; documents as local paths | Local image/document paths in the turn |
|
|
113
187
|
| Change filesystem access from iPhone | Read-only, workspace, or full for the next GrantTap turn | Not exposed; the existing Claude policy remains authoritative |
|
|
114
188
|
| Disable MCP per task | Enforced for later GrantTap-delivered turns | Enforced for later GrantTap-delivered turns |
|
|
115
189
|
| Usage and context | Reported when present in local task logs | Reported when present in local task logs |
|
|
@@ -133,7 +207,7 @@ Repository skills are discovered only in the selected task workspace under
|
|
|
133
207
|
| `ask` | Sends an open question and waits for a spoken or typed reply |
|
|
134
208
|
| `ask_yes_no` | Sends a yes/no question and waits for a tap |
|
|
135
209
|
| `notify` | Sends a non-blocking status update |
|
|
136
|
-
| `setup` | Registers
|
|
210
|
+
| `setup` | Registers Cursor/Claude/Codex policy hooks and the terminal-free helper |
|
|
137
211
|
|
|
138
212
|
The default answer timeout is three minutes. Override it with
|
|
139
213
|
`GRANTTAP_ASK_TIMEOUT_MS`.
|
|
@@ -152,6 +226,14 @@ The default answer timeout is three minutes. Override it with
|
|
|
152
226
|
Durable Objects, and APNs path.
|
|
153
227
|
- APNs carries only a content-neutral wake. It contains no title, prompt,
|
|
154
228
|
command, path, task kind, or response.
|
|
229
|
+
- Cloudflare accepts at most a 32 MiB WebSocket frame. Because task messages are
|
|
230
|
+
sealed and base64-encoded twice, attachments share a 16,000,000-character
|
|
231
|
+
base64 budget (about 12 MB raw total); five small files fit, five 6 MB files do not.
|
|
232
|
+
|
|
233
|
+
The in-chat QR is marked user-only for MCP hosts, but that annotation is not a
|
|
234
|
+
cryptographic separation from the model provider. Use CLI `connect` when the
|
|
235
|
+
model provider itself is part of your threat model; Cloudflare still receives
|
|
236
|
+
only the mailbox id and ciphertext in either flow.
|
|
155
237
|
|
|
156
238
|
The relay can still observe operational metadata: opaque room/mailbox IDs, IP
|
|
157
239
|
addresses, timing, ciphertext sizes, and APNs device token/environment. A
|
|
@@ -181,8 +263,19 @@ time, agent, status, result, and created task ID.
|
|
|
181
263
|
| Command | Purpose |
|
|
182
264
|
| --- | --- |
|
|
183
265
|
| *(no command)* | Starts the GrantTap MCP stdio server |
|
|
266
|
+
| `serve` | HTTP MCP + loopback OAuth for Cursor Settings → Authorize |
|
|
267
|
+
| `authorize` | Installs the persistent loopback OAuth service, verifies health, and configures Cursor |
|
|
184
268
|
| `connect [relayUrl]` | Creates an E2EE pairing; optionally targets a self-hosted `wss://` relay |
|
|
185
|
-
| `setup` | Registers
|
|
269
|
+
| `setup` | Registers Cursor/Claude/Codex hooks, background sync, and repairs configured OAuth |
|
|
270
|
+
| `status [--json]` | Reads local readiness; JSON uses `granttap.provider-status.v1` and contains no keys |
|
|
271
|
+
|
|
272
|
+
The installed public command is `granttap`; `granttap-mcp` remains an alias for
|
|
273
|
+
existing scripts. `status` only reads configuration and runtime state—it does
|
|
274
|
+
not install hooks, pair a device, start OAuth, or reload the background helper.
|
|
275
|
+
If Cursor has an HTTP GrantTap entry, status requires both an owned persistent
|
|
276
|
+
service and a live identity-checked health response; a dead URL is never shown
|
|
277
|
+
as connected. Without an HTTP entry, OAuth remains optional and Cursor policy
|
|
278
|
+
readiness is based on the full hook set, pairing, and background sync.
|
|
186
279
|
|
|
187
280
|
## Development
|
|
188
281
|
|
package/SECURITY.md
CHANGED
|
@@ -18,8 +18,8 @@ to an issue, or pasted into logs. The package avoids printing device secret
|
|
|
18
18
|
keys. Existing configuration files are backed up before hook installation.
|
|
19
19
|
|
|
20
20
|
Current pairings contain three independent secret classes: NaCl endpoint keys,
|
|
21
|
-
random per-task keys, and a random `pushAuth` room credential that
|
|
22
|
-
|
|
21
|
+
random per-task keys, and a random `pushAuth` room credential that authorizes
|
|
22
|
+
the room WebSocket and APNs device-token registration at the relay. None may be logged.
|
|
23
23
|
The relay receives only a hash of `pushAuth` and never receives an endpoint's
|
|
24
24
|
secret encryption key.
|
|
25
25
|
|
|
@@ -31,6 +31,11 @@ secret encryption key.
|
|
|
31
31
|
separate random 256-bit transfer key. Only the mailbox id is used in the HTTP
|
|
32
32
|
path. The key stays in the QR/manual token, so a relay operator or a Durable
|
|
33
33
|
Object database dump cannot open the parked pairing blob.
|
|
34
|
+
- The terminal `connect` flow keeps that transfer key outside model context.
|
|
35
|
+
The MCP `connect` tool deliberately returns a QR in chat for convenience and
|
|
36
|
+
marks it user-only, but MCP audience annotations are not a cryptographic
|
|
37
|
+
boundary: depending on the host, the chat/model provider may receive tool
|
|
38
|
+
images. Use terminal pairing when the model provider is in your threat model.
|
|
34
39
|
- Every attached Codex or Claude Code task receives a separate random 256-bit
|
|
35
40
|
task key. Task messages, attachments, visible activity, access/MCP changes,
|
|
36
41
|
compaction results, and task-bound approvals use this additional authenticated
|
|
@@ -43,6 +48,8 @@ secret encryption key.
|
|
|
43
48
|
transport, network, Cloudflare, Durable Objects, and APNs.
|
|
44
49
|
- APNs is only a content-neutral wake. It contains no task kind, request id,
|
|
45
50
|
delivery id, title, prompt, command, path, or response.
|
|
51
|
+
It is a best-effort silent background notification; after waking, iOS pulls
|
|
52
|
+
and decrypts the queue and creates the single actionable local notification.
|
|
46
53
|
- Scheduler-planner turns and their structured drafts cross Cloudflare only as
|
|
47
54
|
authenticated device-to-device ciphertext. The selected CLI runs locally in
|
|
48
55
|
ephemeral read-only/plan mode; it cannot silently turn planning into a
|
|
@@ -65,7 +72,10 @@ different task's key—not against an already authorized endpoint.
|
|
|
65
72
|
|
|
66
73
|
The delivery ledger and scheduler history are stored only on the paired Mac.
|
|
67
74
|
They contain random message ids and local task metadata and are bounded and
|
|
68
|
-
expired; do not attach real copies to bug reports.
|
|
75
|
+
expired; do not attach real copies to bug reports. Delivery ACK is emitted only
|
|
76
|
+
after a consumer accepts the decrypted payload. A processing lease recovers
|
|
77
|
+
after a crash; agent execution is at-least-once across that rare crash window,
|
|
78
|
+
because third-party Codex/Claude CLIs do not expose a transactional idempotency key.
|
|
69
79
|
|
|
70
80
|
Chat history and capability metadata follow the same cryptographic boundary.
|
|
71
81
|
The bridge reads a bounded set of local Codex/Claude logs, emits only chat
|
|
@@ -113,6 +113,56 @@ export function decisionToCodexOutput(d: ApprovalDecision): unknown {
|
|
|
113
113
|
};
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
// --------------------------------------------------------------------- Cursor
|
|
117
|
+
|
|
118
|
+
export type CursorHookInput = {
|
|
119
|
+
conversation_id?: string;
|
|
120
|
+
generation_id?: string;
|
|
121
|
+
command?: string;
|
|
122
|
+
cwd?: string;
|
|
123
|
+
hook_event_name?: string;
|
|
124
|
+
workspace_roots?: string[];
|
|
125
|
+
session_id?: string;
|
|
126
|
+
tool_name?: string;
|
|
127
|
+
tool_input?: Record<string, unknown>;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export function cursorToRequest(input: CursorHookInput): ApprovalRequest {
|
|
131
|
+
const command = typeof input.command === "string" && input.command.trim()
|
|
132
|
+
? input.command
|
|
133
|
+
: extractCommand("Shell", input.tool_input ?? {});
|
|
134
|
+
const sessionId = input.conversation_id ?? input.session_id;
|
|
135
|
+
const cwd = (typeof input.cwd === "string" && input.cwd.trim() ? input.cwd : undefined)
|
|
136
|
+
?? (Array.isArray(input.workspace_roots) ? input.workspace_roots[0] : undefined);
|
|
137
|
+
return {
|
|
138
|
+
type: "approval.request",
|
|
139
|
+
requestId: randomId(6),
|
|
140
|
+
agent: "cursor",
|
|
141
|
+
kind: "permission",
|
|
142
|
+
tool: input.tool_name ?? "Shell",
|
|
143
|
+
title: shortTitle("Shell", command),
|
|
144
|
+
command,
|
|
145
|
+
cwd,
|
|
146
|
+
sessionId,
|
|
147
|
+
risk: guessRisk("Shell", command),
|
|
148
|
+
createdAt: Date.now(),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function decisionToCursorOutput(d: ApprovalDecision): unknown {
|
|
153
|
+
const permission = d.decision === "allow" ? "allow" : "deny";
|
|
154
|
+
const message = d.note
|
|
155
|
+
?? (d.decision === "allow" ? "Approved from GrantTap" : "Denied from GrantTap");
|
|
156
|
+
return {
|
|
157
|
+
permission,
|
|
158
|
+
continue: d.decision === "allow",
|
|
159
|
+
user_message: message,
|
|
160
|
+
agent_message: message,
|
|
161
|
+
userMessage: message,
|
|
162
|
+
agentMessage: message,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
// ------------------------------------------------------------------ helpers
|
|
117
167
|
|
|
118
168
|
function shortTitle(tool: string, command: string): string {
|