granttap-mcp 0.1.0 → 0.2.0
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 +32 -6
- package/SECURITY.md +21 -0
- package/apps/bridge/src/bin/connect.ts +109 -0
- package/bin/granttap-mcp.mjs +3 -0
- package/docs/images/phone-activity.png +0 -0
- package/docs/images/watch-approval.png +0 -0
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -14,6 +14,15 @@ GrantTap MCP complements the agents' native permission hooks:
|
|
|
14
14
|
a tool call and waits for Allow or Deny.
|
|
15
15
|
- Both channels use the same end-to-end encrypted GrantTap pairing.
|
|
16
16
|
|
|
17
|
+
| Visible iPhone activity | Approval on Apple Watch |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
|  |  |
|
|
20
|
+
|
|
21
|
+
The iPhone and Apple Watch app is preparing for App Store release. Current
|
|
22
|
+
status and target date are published at [granttap.com](https://granttap.com).
|
|
23
|
+
Support, privacy, terms, and license information are linked below and from the
|
|
24
|
+
app's About screen.
|
|
25
|
+
|
|
17
26
|
## Install
|
|
18
27
|
|
|
19
28
|
The shortest MCP-only setup is:
|
|
@@ -23,18 +32,23 @@ codex mcp add granttap -- npx -y granttap-mcp
|
|
|
23
32
|
claude mcp add granttap -- npx -y granttap-mcp
|
|
24
33
|
```
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
stable, then register both supported agents:
|
|
35
|
+
Pair this computer with the GrantTap app, then register the approval hooks:
|
|
28
36
|
|
|
29
37
|
```bash
|
|
30
38
|
npm install -g granttap-mcp
|
|
39
|
+
granttap-mcp connect
|
|
31
40
|
granttap-mcp setup
|
|
32
41
|
```
|
|
33
42
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
the
|
|
37
|
-
|
|
43
|
+
`connect` prints a QR and an optional one-time eight-character code. It uses
|
|
44
|
+
the production zero-knowledge relay by default; pass your own `wss://` URL as
|
|
45
|
+
the first argument to self-host. The pairing lives locally in
|
|
46
|
+
`~/.granttap/machine.json`; this package never uploads the device secret key.
|
|
47
|
+
Existing beta installations using `~/.nodvox/` are migrated automatically.
|
|
48
|
+
|
|
49
|
+
`connect` also registers both hooks. `setup` is an idempotent standalone
|
|
50
|
+
command for upgrades: it preserves unrelated settings and writes a backup
|
|
51
|
+
before changing a configuration file.
|
|
38
52
|
|
|
39
53
|
## MCP tools
|
|
40
54
|
|
|
@@ -45,6 +59,14 @@ automatically.
|
|
|
45
59
|
| `notify` | Sends a non-blocking status message |
|
|
46
60
|
| `setup` | Registers the Claude Code and Codex approval hooks |
|
|
47
61
|
|
|
62
|
+
## CLI commands
|
|
63
|
+
|
|
64
|
+
| Command | Purpose |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| *(no command)* | Starts the GrantTap MCP stdio server |
|
|
67
|
+
| `connect [relayUrl]` | Creates an E2EE pairing and prints a QR/short code |
|
|
68
|
+
| `setup` | Registers the Claude Code and Codex approval hooks |
|
|
69
|
+
|
|
48
70
|
The default answer timeout is three minutes. Override it with
|
|
49
71
|
`GRANTTAP_ASK_TIMEOUT_MS`.
|
|
50
72
|
|
|
@@ -83,6 +105,10 @@ as `*.bak-granttap`.
|
|
|
83
105
|
## Related
|
|
84
106
|
|
|
85
107
|
- Product: [granttap.com](https://granttap.com)
|
|
108
|
+
- npm: [granttap-mcp](https://www.npmjs.com/package/granttap-mcp)
|
|
86
109
|
- Relay: [sergii-ziborov/granttap-relay](https://github.com/sergii-ziborov/granttap-relay)
|
|
110
|
+
- Privacy: [granttap.com/privacy](https://granttap.com/privacy)
|
|
111
|
+
- Support: [granttap.com/support](https://granttap.com/support)
|
|
112
|
+
- Security policy: [SECURITY.md](SECURITY.md)
|
|
87
113
|
|
|
88
114
|
GrantTap is not affiliated with Anthropic or OpenAI.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Use GitHub's private
|
|
6
|
+
[security advisory form](https://github.com/sergii-ziborov/granttap-mcp/security/advisories/new).
|
|
7
|
+
For urgent coordination, contact `sergii.ziborov@gmail.com`.
|
|
8
|
+
|
|
9
|
+
Do not open a public issue containing pairing codes, device keys, room
|
|
10
|
+
identifiers, real prompts, command payloads, local configuration, or a working
|
|
11
|
+
exploit. Include the affected package version or commit, expected impact, and
|
|
12
|
+
a minimal reproduction using synthetic data.
|
|
13
|
+
|
|
14
|
+
## Local secrets
|
|
15
|
+
|
|
16
|
+
Pairing state belongs in `~/.granttap/` and must never be committed, attached
|
|
17
|
+
to an issue, or pasted into logs. The package avoids printing device secret
|
|
18
|
+
keys. Existing configuration files are backed up before hook installation.
|
|
19
|
+
|
|
20
|
+
Relay vulnerabilities should be reported through the
|
|
21
|
+
[GrantTap relay advisory form](https://github.com/sergii-ziborov/granttap-relay/security/advisories/new).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import QRCode from "qrcode";
|
|
2
|
+
import { generatePairingCode, sealWithCode } from "../../../../packages/core/crypto";
|
|
3
|
+
import { installClaudeHook, installCodexHook, type InstallResult } from "../install";
|
|
4
|
+
import {
|
|
5
|
+
createPairing,
|
|
6
|
+
machineConfigPath,
|
|
7
|
+
pairingUri,
|
|
8
|
+
phonePairingPath,
|
|
9
|
+
saveConfig,
|
|
10
|
+
} from "../config";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_RELAY = "wss://granttap-relay.sergii-ziborov.workers.dev";
|
|
13
|
+
|
|
14
|
+
function hookLine(name: string, result: InstallResult): string {
|
|
15
|
+
switch (result.status) {
|
|
16
|
+
case "installed":
|
|
17
|
+
return ` ✓ ${name}: hook installed (${result.detail}; backup: .bak-granttap)`;
|
|
18
|
+
case "already":
|
|
19
|
+
return ` ✓ ${name}: hook already installed (${result.detail})`;
|
|
20
|
+
case "manual":
|
|
21
|
+
return ` ! ${name}: ${result.detail}`;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function connect(relayUrl: string): Promise<void> {
|
|
26
|
+
const { machineCfg, phoneCfg } = createPairing(relayUrl);
|
|
27
|
+
saveConfig(machineConfigPath(), machineCfg);
|
|
28
|
+
saveConfig(phonePairingPath(), phoneCfg);
|
|
29
|
+
|
|
30
|
+
const uri = pairingUri(phoneCfg);
|
|
31
|
+
const qr = await QRCode.toString(uri, {
|
|
32
|
+
type: "terminal",
|
|
33
|
+
small: true,
|
|
34
|
+
errorCorrectionLevel: "L",
|
|
35
|
+
});
|
|
36
|
+
process.stdout.write(["", " Scan this code in GrantTap on iPhone:", "", qr].join("\n"));
|
|
37
|
+
|
|
38
|
+
const code = generatePairingCode(8);
|
|
39
|
+
const sealed = sealWithCode(phoneCfg, code);
|
|
40
|
+
const httpBase = relayUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:");
|
|
41
|
+
let codeReady = false;
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(`${httpBase}/pair/${code}`, {
|
|
44
|
+
method: "PUT",
|
|
45
|
+
headers: { "content-type": "application/json" },
|
|
46
|
+
body: JSON.stringify(sealed),
|
|
47
|
+
});
|
|
48
|
+
codeReady = response.ok;
|
|
49
|
+
} catch {
|
|
50
|
+
codeReady = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
process.stdout.write(
|
|
54
|
+
codeReady
|
|
55
|
+
? [
|
|
56
|
+
"",
|
|
57
|
+
" Or enter this one-time code in the app:",
|
|
58
|
+
"",
|
|
59
|
+
` ${code.slice(0, 4)} ${code.slice(4)}`,
|
|
60
|
+
"",
|
|
61
|
+
` Relay: ${httpBase}`,
|
|
62
|
+
" The code expires after 15 minutes and can be used once.",
|
|
63
|
+
"",
|
|
64
|
+
].join("\n")
|
|
65
|
+
: [
|
|
66
|
+
"",
|
|
67
|
+
` ! The relay at ${httpBase} did not accept a short code.`,
|
|
68
|
+
" The QR still works. Check the relay and retry for code pairing.",
|
|
69
|
+
"",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const skipHooks = process.env.GRANTTAP_SKIP_HOOKS === "1";
|
|
74
|
+
const claude = skipHooks ? null : installClaudeHook();
|
|
75
|
+
const codex = skipHooks ? null : installCodexHook();
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
[
|
|
78
|
+
"",
|
|
79
|
+
" GrantTap is paired on this machine.",
|
|
80
|
+
"",
|
|
81
|
+
` room: ${machineCfg.room}`,
|
|
82
|
+
` relay: ${relayUrl}`,
|
|
83
|
+
` machine cfg: ${machineConfigPath()}`,
|
|
84
|
+
` phone cfg: ${phonePairingPath()} (manual fallback; keep private)`,
|
|
85
|
+
"",
|
|
86
|
+
...(skipHooks
|
|
87
|
+
? [" Hooks: skipped (GRANTTAP_SKIP_HOOKS=1)"]
|
|
88
|
+
: [hookLine("Claude Code", claude!), hookLine("Codex", codex!)]),
|
|
89
|
+
"",
|
|
90
|
+
" Keep the MCP server configured in your agent. Approval requests now",
|
|
91
|
+
" use the paired phone and watch; if the relay is unavailable, hooks fall",
|
|
92
|
+
" back to the agent's normal local approval flow.",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const relayUrl =
|
|
99
|
+
process.argv[2] ??
|
|
100
|
+
process.env.GRANTTAP_RELAY_URL ??
|
|
101
|
+
process.env.NODVOX_RELAY_URL ??
|
|
102
|
+
DEFAULT_RELAY;
|
|
103
|
+
|
|
104
|
+
void connect(relayUrl).catch((error: unknown) => {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`[granttap-mcp] connect failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
107
|
+
);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
});
|
package/bin/granttap-mcp.mjs
CHANGED
|
@@ -11,6 +11,8 @@ if (command === "mcp") {
|
|
|
11
11
|
entry = join(root, "apps", "mcp", "src", "server.ts");
|
|
12
12
|
} else if (command === "setup") {
|
|
13
13
|
entry = join(root, "apps", "bridge", "src", "bin", "setup.ts");
|
|
14
|
+
} else if (command === "connect") {
|
|
15
|
+
entry = join(root, "apps", "bridge", "src", "bin", "connect.ts");
|
|
14
16
|
} else if (command === "hook" && (argument === "claude" || argument === "codex")) {
|
|
15
17
|
entry = join(root, "apps", "bridge", "src", "bin", `${argument}-hook.ts`);
|
|
16
18
|
} else {
|
|
@@ -18,6 +20,7 @@ if (command === "mcp") {
|
|
|
18
20
|
[
|
|
19
21
|
"Usage:",
|
|
20
22
|
" granttap-mcp Start the MCP stdio server",
|
|
23
|
+
" granttap-mcp connect [url] Pair this machine with iPhone/Apple Watch",
|
|
21
24
|
" granttap-mcp setup Install Claude Code and Codex approval hooks",
|
|
22
25
|
" granttap-mcp hook <agent> Internal hook entry point",
|
|
23
26
|
"",
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "E2EE MCP bridge and approval hooks for GrantTap on iPhone and Apple Watch.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"granttap-mcp": "
|
|
7
|
+
"granttap-mcp": "bin/granttap-mcp.mjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"apps",
|
|
11
11
|
"bin",
|
|
12
|
+
"docs",
|
|
12
13
|
"packages",
|
|
13
14
|
"LICENSE",
|
|
14
|
-
"README.md"
|
|
15
|
+
"README.md",
|
|
16
|
+
"SECURITY.md"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"start": "node bin/granttap-mcp.mjs",
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
27
|
+
"qrcode": "^1.5.4",
|
|
25
28
|
"tsx": "^4.19.2",
|
|
26
29
|
"tweetnacl": "^1.0.3",
|
|
27
30
|
"ws": "^8.18.0",
|
|
@@ -29,6 +32,7 @@
|
|
|
29
32
|
},
|
|
30
33
|
"devDependencies": {
|
|
31
34
|
"@types/node": "^22.10.2",
|
|
35
|
+
"@types/qrcode": "^1.5.6",
|
|
32
36
|
"@types/ws": "^8.5.13",
|
|
33
37
|
"typescript": "^5.7.2"
|
|
34
38
|
},
|