granttap-mcp 0.8.10 → 0.8.12
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 +2 -7
- package/apps/bridge/src/bin/setup.ts +16 -12
- package/apps/mcp/src/bin/authorize.ts +2 -2
- package/apps/mcp/src/bin/serve.ts +2 -2
- package/apps/mcp/src/connection-center/widget.html +2 -2
- package/apps/mcp/src/cursor-config.ts +2 -2
- package/apps/mcp/src/http-server.ts +67 -2
- package/apps/mcp/src/http-service/installer.ts +3 -3
- package/apps/mcp/src/oauth/README.md +1 -1
- package/apps/mcp/src/oauth/loopback-origin.ts +1 -0
- package/apps/mcp/src/oauth-provider.ts +5 -13
- package/cursor-plugin/.cursor-plugin/marketplace.json +3 -3
- package/cursor-plugin/.cursor-plugin/plugin.json +2 -2
- package/cursor-plugin/README.md +12 -25
- package/cursor-plugin/commands/connect.md +4 -5
- package/cursor-plugin/skills/connect/SKILL.md +15 -17
- package/docs/cursor-authorize.md +2 -2
- package/package.json +1 -1
- package/apps/mcp/src/oauth/consent-page.ts +0 -138
package/README.md
CHANGED
|
@@ -224,13 +224,8 @@ grok plugin install granttap --trust
|
|
|
224
224
|
```
|
|
225
225
|
|
|
226
226
|
Install **GrantTap** from the Cursor plugin marketplace, then open
|
|
227
|
-
**Cursor
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
mkdir -p "$HOME/.cursor/plugins/local"
|
|
232
|
-
ln -sfn "$PWD/cursor-plugin" "$HOME/.cursor/plugins/local/granttap"
|
|
233
|
-
```
|
|
227
|
+
**Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
|
|
228
|
+
and any one-time QR pairing on `https://granttap.com/connect`.
|
|
234
229
|
|
|
235
230
|
Open GrantTap from the plugin page or ask `Show my GrantTap pairing QR.` Codex
|
|
236
231
|
renders an interactive connection card with pairing status, a one-time QR,
|
|
@@ -62,12 +62,12 @@ async function main(): Promise<void> {
|
|
|
62
62
|
engineFlag >= 0 ? process.argv[engineFlag + 1] : undefined,
|
|
63
63
|
);
|
|
64
64
|
const helper = installMonitorHelper();
|
|
65
|
-
let
|
|
65
|
+
let httpService: InstallResult | null = null;
|
|
66
66
|
let cursorConfig: InstallResult | null = null;
|
|
67
|
-
const
|
|
68
|
-
if (before.cursor.installed ||
|
|
69
|
-
|
|
70
|
-
if (before.cursor.installed &&
|
|
67
|
+
const mcpClientInstalled = before.agents.some((item) => item.installed);
|
|
68
|
+
if (before.cursor.installed || mcpClientInstalled) {
|
|
69
|
+
httpService = installHttpMcpService();
|
|
70
|
+
if (before.cursor.installed && httpService.status !== "manual") {
|
|
71
71
|
cursorConfig = installCursorHttpConfig();
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -76,7 +76,7 @@ async function main(): Promise<void> {
|
|
|
76
76
|
const installed = new Set(before.agents.filter((item) => item.installed).map((item) => item.agent));
|
|
77
77
|
const cursorReady = cursorHook.status !== "manual"
|
|
78
78
|
&& (!before.cursor.installed
|
|
79
|
-
|| (
|
|
79
|
+
|| (httpService?.status !== "manual" && cursorConfig?.status !== "manual"));
|
|
80
80
|
|
|
81
81
|
process.stdout.write([
|
|
82
82
|
"GrantTap",
|
|
@@ -84,13 +84,17 @@ async function main(): Promise<void> {
|
|
|
84
84
|
`Phone pairing ${paired ? "Ready" : pairingResult === "failed" ? "Relay unavailable" : "Needs connection"}`,
|
|
85
85
|
`Background helper ${state(helper)}`,
|
|
86
86
|
"",
|
|
87
|
-
`Claude Code ${installed.has("claude")
|
|
87
|
+
`Claude Code ${installed.has("claude")
|
|
88
|
+
? claudeHook.status === "manual" || httpService?.status === "manual"
|
|
89
|
+
? "Needs attention" : "Authorize in Claude Code" : "Not installed"}`,
|
|
88
90
|
`Codex ${installed.has("codex")
|
|
89
|
-
? codexHook.status === "manual" ||
|
|
91
|
+
? codexHook.status === "manual" || httpService?.status === "manual"
|
|
90
92
|
? "Needs attention" : !paired ? "Needs connection" : "Authorize in Codex; review hooks"
|
|
91
93
|
: "Not installed"}`,
|
|
92
|
-
`Cursor ${before.cursor.installed ? `Beta · ${cursorReady ? "
|
|
93
|
-
`Grok Build ${installed.has("grok")
|
|
94
|
+
`Cursor ${before.cursor.installed ? `Beta · ${cursorReady ? "Authenticate in Cursor" : "Needs repair"}` : "Not installed"}`,
|
|
95
|
+
`Grok Build ${installed.has("grok")
|
|
96
|
+
? httpService?.status === "manual" ? "Needs attention" : "Authorize in Grok Build"
|
|
97
|
+
: "Not installed"}`,
|
|
94
98
|
// Governance is edited on the phone but cannot report until an engine is
|
|
95
99
|
// declared here, so say plainly which of the two is missing.
|
|
96
100
|
`Project Governance ${engine ? "Ready" : "No engine found"}`,
|
|
@@ -98,9 +102,9 @@ async function main(): Promise<void> {
|
|
|
98
102
|
paired && installed.has("codex")
|
|
99
103
|
? `Next: authorize GrantTap in Codex, then ${CODEX_TRUST_INSTRUCTION}`
|
|
100
104
|
: paired && before.cursor.installed
|
|
101
|
-
? "Next: open Cursor
|
|
105
|
+
? "Next: open Cursor Customize → MCPs → GrantTap → Authenticate."
|
|
102
106
|
: !paired
|
|
103
|
-
? "Next:
|
|
107
|
+
? "Next: authenticate GrantTap in your coding app; granttap.com/connect will show the pairing QR."
|
|
104
108
|
: engine
|
|
105
109
|
? "Governance is live. Restart the helper to pick it up: launchctl kickstart -k gui/$(id -u)/com.granttap.monitor"
|
|
106
110
|
: "Governance stays off until an engine is found. Point at one with: granttap setup --engine <path>",
|
|
@@ -71,8 +71,8 @@ async function authorize(): Promise<void> {
|
|
|
71
71
|
`[granttap-mcp] Cursor · Configured — ${cursor.detail}`,
|
|
72
72
|
`[granttap-mcp] Persistent local OAuth is healthy at ${mcpUrl}`,
|
|
73
73
|
`[granttap-mcp] LaunchAgent: ${service.status} (${service.detail})`,
|
|
74
|
-
"[granttap-mcp] Open Cursor
|
|
75
|
-
"[granttap-mcp] If this Mac is not paired,
|
|
74
|
+
"[granttap-mcp] Open Cursor Customize → MCPs → GrantTap → Authenticate. The browser continues at granttap.com/connect.",
|
|
75
|
+
"[granttap-mcp] If this Mac is not paired, granttap.com/connect shows a QR and manual token.",
|
|
76
76
|
"",
|
|
77
77
|
].join("\n"),
|
|
78
78
|
);
|
|
@@ -15,9 +15,9 @@ async function serve(): Promise<void> {
|
|
|
15
15
|
process.stderr.write(
|
|
16
16
|
[
|
|
17
17
|
`[granttap-mcp] HTTP MCP + OAuth listening on ${started.mcpUrl}`,
|
|
18
|
-
"[granttap-mcp] Cursor
|
|
18
|
+
"[granttap-mcp] Cursor authentication: set ~/.cursor/mcp.json entry to:",
|
|
19
19
|
` "granttap": { "url": "${started.mcpUrl}" }`,
|
|
20
|
-
"[granttap-mcp] Then open Cursor
|
|
20
|
+
"[granttap-mcp] Then open Cursor Customize → MCPs → GrantTap → Authenticate and continue at granttap.com/connect.",
|
|
21
21
|
"",
|
|
22
22
|
].join("\n"),
|
|
23
23
|
);
|
|
@@ -37,7 +37,7 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
|
|
|
37
37
|
<h3>Provider readiness</h3><ul id="providers"></ul>
|
|
38
38
|
<p>No GrantTap account or password is required. Pair using GrantTap on iPhone → Settings → Connections.
|
|
39
39
|
Provider sign-in is separate.</p>
|
|
40
|
-
<p>Missing hooks or background sync? Run <code>
|
|
40
|
+
<p>Missing hooks or background sync? Run <code>npm install -g granttap-mcp@0.8.12</code> and <code>granttap setup</code> locally.
|
|
41
41
|
A saved pairing alone does not prove your phone or the background helper is online.</p>
|
|
42
42
|
<p><a href="https://granttap.com" target="_blank" rel="noopener noreferrer">Website</a> ·
|
|
43
43
|
<a href="https://granttap.com/privacy" target="_blank" rel="noopener noreferrer">Privacy</a> ·
|
|
@@ -47,4 +47,4 @@ details{border-top:1px solid #8884;padding-top:14px}summary{cursor:pointer;font-
|
|
|
47
47
|
</main>
|
|
48
48
|
<script type="module">{{SCRIPT}}</script>
|
|
49
49
|
</body>
|
|
50
|
-
</html>
|
|
50
|
+
</html>
|
|
@@ -97,7 +97,7 @@ export function inspectCursorHttpConfig(
|
|
|
97
97
|
}
|
|
98
98
|
const config = parseConfig(path);
|
|
99
99
|
if (!config) {
|
|
100
|
-
return { status: "action_required", detail: "Cursor mcp.json is invalid JSON; fix it before
|
|
100
|
+
return { status: "action_required", detail: "Cursor mcp.json is invalid JSON; fix it before authentication." };
|
|
101
101
|
}
|
|
102
102
|
const servers = config.mcpServers;
|
|
103
103
|
if (servers == null) {
|
|
@@ -108,7 +108,7 @@ export function inspectCursorHttpConfig(
|
|
|
108
108
|
}
|
|
109
109
|
const entry = (servers as Record<string, unknown>).granttap;
|
|
110
110
|
if (isHttpEntry(entry, expectedUrl)) {
|
|
111
|
-
return { status: "action_required", detail: "Endpoint configured; keep local OAuth running and
|
|
111
|
+
return { status: "action_required", detail: "Endpoint configured; keep local OAuth running and complete authentication in Cursor." };
|
|
112
112
|
}
|
|
113
113
|
return entry == null
|
|
114
114
|
? { status: "not_configured", detail: "Run granttap setup to add Cursor." }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GrantTap MCP over Streamable HTTP + loopback OAuth.
|
|
2
|
+
* GrantTap MCP over Streamable HTTP + loopback OAuth with website consent.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Codex, Claude Code, Cursor and Grok Build can discover this OAuth endpoint.
|
|
5
5
|
* Bind is loopback-only. Pairing keys remain in ~/.granttap.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -16,6 +16,7 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
|
16
16
|
import express from "express";
|
|
17
17
|
import QRCode from "qrcode";
|
|
18
18
|
import { createOneTimePairing, DEFAULT_RELAY } from "../../bridge/src/pairing";
|
|
19
|
+
import { inspectAgentIntegrations } from "../../bridge/src/install";
|
|
19
20
|
import { createGrantTapServer, relay, resetRelay } from "./create-server";
|
|
20
21
|
import { isAllowedLoopbackOrigin } from "./oauth/loopback-origin";
|
|
21
22
|
import { GrantTapOAuthProvider } from "./oauth-provider";
|
|
@@ -62,6 +63,48 @@ export async function startHttpMcpServer(options: ServeOptions = {}): Promise<{
|
|
|
62
63
|
serviceDocumentationUrl: new URL("https://granttap.com"),
|
|
63
64
|
}));
|
|
64
65
|
|
|
66
|
+
// The public website is the only browser origin allowed to read a pending
|
|
67
|
+
// authorization. The random pending id lives in the website URL fragment,
|
|
68
|
+
// never in a request to the website server.
|
|
69
|
+
app.use(["/oauth/session", "/oauth/pairing", "/oauth/decision"], (req, res, next) => {
|
|
70
|
+
const origin = req.get("origin");
|
|
71
|
+
if (origin && origin !== "https://granttap.com"
|
|
72
|
+
&& !isAllowedLoopbackOrigin(origin, issuerUrl.origin)) {
|
|
73
|
+
res.status(403).json({ error: "Origin is not allowed." });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (origin === "https://granttap.com") {
|
|
77
|
+
res.set({
|
|
78
|
+
"Access-Control-Allow-Origin": origin,
|
|
79
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
80
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
81
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
82
|
+
"Vary": "Origin, Access-Control-Request-Private-Network",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (req.method === "OPTIONS") {
|
|
86
|
+
res.status(204).end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
next();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
app.get("/oauth/session", (req, res) => {
|
|
93
|
+
res.set("Cache-Control", "no-store");
|
|
94
|
+
const pending = provider.getPending(String(req.query.pending_id ?? ""));
|
|
95
|
+
if (!pending) {
|
|
96
|
+
res.status(404).json({ error: "This connection request expired. Start again in your coding app." });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
res.json({
|
|
100
|
+
clientName: pending.client.client_name?.trim().slice(0, 80) || "Coding app",
|
|
101
|
+
paired: isMachineConfigured(),
|
|
102
|
+
providers: inspectAgentIntegrations()
|
|
103
|
+
.filter((item) => ["codex", "claude", "cursor"].includes(item.agent))
|
|
104
|
+
.map((item) => ({ id: item.agent, installed: item.installed, ready: item.hookConfigured })),
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
65
108
|
app.post("/oauth/pairing", async (req, res) => {
|
|
66
109
|
res.set("Cache-Control", "no-store");
|
|
67
110
|
try {
|
|
@@ -115,6 +158,28 @@ export async function startHttpMcpServer(options: ServeOptions = {}): Promise<{
|
|
|
115
158
|
}
|
|
116
159
|
});
|
|
117
160
|
|
|
161
|
+
app.post("/oauth/decision", (req, res) => {
|
|
162
|
+
res.set("Cache-Control", "no-store");
|
|
163
|
+
if (req.get("origin") !== "https://granttap.com") {
|
|
164
|
+
res.status(403).json({ error: "Origin is not allowed." });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const pendingId = String(req.body?.pending_id ?? "");
|
|
169
|
+
const decision = String(req.body?.decision ?? "");
|
|
170
|
+
if (decision !== "approve" && decision !== "deny") {
|
|
171
|
+
res.status(400).json({ error: "Choose Approve or Deny." });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const { redirectUrl } = provider.completeConsent(pendingId, decision === "approve");
|
|
175
|
+
res.json({ redirectUrl });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
res.status(400).json({
|
|
178
|
+
error: error instanceof Error ? error.message : String(error),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
118
183
|
app.post("/consent", (req, res) => {
|
|
119
184
|
res.set("Cache-Control", "no-store");
|
|
120
185
|
try {
|
|
@@ -27,7 +27,7 @@ export function installHttpMcpService(options: { forceReload?: boolean } = {}):
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function macosOnly(): InstallResult {
|
|
30
|
-
return { status: "manual", detail: "
|
|
30
|
+
return { status: "manual", detail: "Persistent GrantTap MCP authorization currently requires macOS" };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
function configuredMcpUrl(): URL | Error {
|
|
@@ -42,13 +42,13 @@ function safeNodeBin(): string | Error {
|
|
|
42
42
|
const nodeBin = resolveMonitorNodeBin();
|
|
43
43
|
return nodeBin && !isCursorHelperNode(nodeBin)
|
|
44
44
|
? nodeBin
|
|
45
|
-
: new Error("No safe absolute Node binary is available
|
|
45
|
+
: new Error("No safe absolute Node binary is available for the persistent GrantTap service.");
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function validateInstallation(nodeBin: string, executable: string): InstallResult | undefined {
|
|
49
49
|
if (!existsSync(executable)) return { status: "manual", detail: `Installed launcher is missing at ${executable}` };
|
|
50
50
|
if (packageRoot.includes("/.npm/_npx/")) {
|
|
51
|
-
return { status: "manual", detail: "
|
|
51
|
+
return { status: "manual", detail: "Authorization requires a stable granttap-mcp install; npm's temporary _npx cache is not durable." };
|
|
52
52
|
}
|
|
53
53
|
const version = spawnSync(nodeBin, ["--version"], { encoding: "utf8" });
|
|
54
54
|
const major = Number(/^v(\d+)/.exec(version.stdout.trim())?.[1]);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# OAuth
|
|
2
2
|
|
|
3
|
-
This module owns local
|
|
3
|
+
This module owns local MCP OAuth client persistence and token issuance. The OAuth authorization endpoint immediately redirects to `https://granttap.com/connect`; the website reads a short-lived request through the loopback helper, while pairing keys remain on the computer and phone. The local service never renders a user-facing consent page.
|
|
4
4
|
|
|
5
5
|
License: this module is distributed under the GrantTap Commercial Source License in the repository-root `LICENSE` file.
|
|
@@ -9,6 +9,7 @@ export function isAllowedLoopbackOrigin(
|
|
|
9
9
|
): boolean {
|
|
10
10
|
if (!origin) return true;
|
|
11
11
|
if (origin === issuerOrigin) return true;
|
|
12
|
+
if (origin === "https://granttap.com") return true;
|
|
12
13
|
const lower = origin.toLowerCase();
|
|
13
14
|
return lower.startsWith("vscode-webview:")
|
|
14
15
|
|| lower.startsWith("cursor:")
|
|
@@ -22,7 +22,6 @@ import type {
|
|
|
22
22
|
OAuthTokens,
|
|
23
23
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
24
24
|
import { isMachineConfigured } from "./pairing-status";
|
|
25
|
-
import { consentHtml } from "./oauth/consent-page";
|
|
26
25
|
import { GrantTapClientsStore, loadOAuthStore, saveOAuthStore } from "./oauth/store";
|
|
27
26
|
|
|
28
27
|
type PendingAuth = {
|
|
@@ -48,7 +47,7 @@ export class GrantTapOAuthProvider implements OAuthServerProvider {
|
|
|
48
47
|
|
|
49
48
|
constructor(private readonly expectedResource?: string) {}
|
|
50
49
|
|
|
51
|
-
/** Begin auth: stash params
|
|
50
|
+
/** Begin auth: stash params; the website reads only the opaque request id. */
|
|
52
51
|
createPending(client: OAuthClientInformationFull, params: AuthorizationParams): string {
|
|
53
52
|
this.gcPending();
|
|
54
53
|
const id = randomUUID();
|
|
@@ -86,22 +85,15 @@ export class GrantTapOAuthProvider implements OAuthServerProvider {
|
|
|
86
85
|
resource: this.expectedResource ? new URL(this.expectedResource) : params.resource,
|
|
87
86
|
};
|
|
88
87
|
const pendingId = this.createPending(client, normalizedParams);
|
|
89
|
-
const paired = isMachineConfigured();
|
|
90
88
|
res.set({
|
|
91
89
|
"Cache-Control": "no-store",
|
|
92
|
-
"Content-Security-Policy": "frame-ancestors 'none'; base-uri 'none'; form-action 'self'",
|
|
93
90
|
"Referrer-Policy": "no-referrer",
|
|
94
|
-
"X-Frame-Options": "DENY",
|
|
95
91
|
"X-Content-Type-Options": "nosniff",
|
|
96
92
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
redirectUri: normalizedParams.redirectUri,
|
|
102
|
-
scopes: normalizedParams.scopes ?? [],
|
|
103
|
-
resource: normalizedParams.resource?.href ?? "local GrantTap MCP",
|
|
104
|
-
}));
|
|
93
|
+
const website = new URL("https://granttap.com/connect");
|
|
94
|
+
const resource = new URL(this.expectedResource ?? normalizedParams.resource!.href);
|
|
95
|
+
website.hash = new URLSearchParams({ request: pendingId, port: resource.port }).toString();
|
|
96
|
+
res.redirect(302, website.href);
|
|
105
97
|
}
|
|
106
98
|
|
|
107
99
|
/** Complete consent: issue code and redirect to the requesting MCP client. */
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
"email": "support@granttap.com"
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
|
-
"description": "GrantTap for Cursor —
|
|
9
|
-
"version": "0.1.
|
|
8
|
+
"description": "GrantTap for Cursor — authorize on granttap.com/connect and approve local tasks from your phone",
|
|
9
|
+
"version": "0.1.5"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "granttap",
|
|
14
14
|
"source": "./",
|
|
15
|
-
"description": "
|
|
15
|
+
"description": "Authorize Cursor on granttap.com/connect, pair by QR, and keep each approval in its originating chat"
|
|
16
16
|
}
|
|
17
17
|
]
|
|
18
18
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap",
|
|
3
3
|
"displayName": "GrantTap",
|
|
4
|
-
"version": "0.1.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "0.1.5",
|
|
5
|
+
"description": "Connect Cursor to GrantTap through granttap.com/connect, pair your phone with a one-time QR, and keep approvals in the originating chat.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "GrantTap",
|
|
8
8
|
"email": "support@granttap.com",
|
package/cursor-plugin/README.md
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# GrantTap Cursor plugin
|
|
2
2
|
|
|
3
3
|
GrantTap connects Cursor to the GrantTap phone app through a loopback-only HTTP
|
|
4
|
-
MCP server. Cursor can show its native **
|
|
4
|
+
MCP server. Cursor can show its native **Authenticate** action, while GrantTap keeps
|
|
5
5
|
each phone response tied to the exact originating chat and prompt.
|
|
6
6
|
|
|
7
7
|
## Connect Cursor
|
|
8
8
|
|
|
9
|
-
Install the supported CLI
|
|
9
|
+
Install the supported CLI and the GrantTap plugin from Cursor's marketplace,
|
|
10
|
+
then run:
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
npm install -g granttap-mcp
|
|
13
14
|
granttap setup
|
|
14
|
-
granttap connect
|
|
15
|
-
granttap setup
|
|
16
15
|
granttap status
|
|
17
16
|
```
|
|
18
17
|
|
|
@@ -20,20 +19,17 @@ The commands have separate jobs:
|
|
|
20
19
|
|
|
21
20
|
1. `granttap setup` detects Cursor, installs and verifies the persistent loopback OAuth/MCP
|
|
22
21
|
service, then writes only GrantTap's entry in `~/.cursor/mcp.json`.
|
|
23
|
-
2. `granttap
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
**Authorize**. The local consent page confirms access to this Mac's GrantTap
|
|
32
|
-
pairing. If the Mac is not paired, it offers the same one-time QR and manual-code
|
|
33
|
-
fallback as the CLI.
|
|
22
|
+
2. `granttap setup` also installs the supported policy hooks and background
|
|
23
|
+
task-sync helper. OAuth remains a separate authorization.
|
|
24
|
+
3. `granttap status` performs a read-only readiness check.
|
|
25
|
+
|
|
26
|
+
After `granttap setup`, open **Cursor Customize → MCPs → GrantTap** and choose
|
|
27
|
+
**Authenticate**. The browser opens granttap.com/connect to authorize Cursor.
|
|
28
|
+
If the Mac is not paired, the website shows a one-time QR and manual-code
|
|
29
|
+
fallback; a saved pairing is reused unless you explicitly confirm reconnect.
|
|
34
30
|
|
|
35
31
|
The OAuth service listens only at `http://127.0.0.1:17342/mcp`. Cursor cannot
|
|
36
|
-
show **
|
|
32
|
+
show **Authenticate** for a stdio (`command`/`args`) MCP entry, so do not replace the
|
|
37
33
|
plugin's HTTP configuration with stdio.
|
|
38
34
|
|
|
39
35
|
## Install from the Cursor marketplace
|
|
@@ -43,15 +39,6 @@ The plugin adds the `/connect` command, a connect skill, and the
|
|
|
43
39
|
exact-correlation rule; `granttap setup` still configures the MCP endpoint
|
|
44
40
|
itself.
|
|
45
41
|
|
|
46
|
-
From a source checkout, the same plugin can be linked locally:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
mkdir -p "$HOME/.cursor/plugins/local"
|
|
50
|
-
ln -sfn "$PWD/cursor-plugin" "$HOME/.cursor/plugins/local/granttap"
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Run those commands from the repository root.
|
|
54
|
-
|
|
55
42
|
## Exact dual-channel behavior
|
|
56
43
|
|
|
57
44
|
For an interactive question or approval, Cursor and the phone receive the same
|
|
@@ -7,13 +7,12 @@ end-to-end.
|
|
|
7
7
|
|
|
8
8
|
1. Run `granttap setup` to detect Cursor and configure the persistent loopback HTTP
|
|
9
9
|
MCP/OAuth service.
|
|
10
|
-
2. Open **Cursor
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
4. Run `granttap setup`, then verify the result with `granttap status`.
|
|
10
|
+
2. Open **Cursor Customize → MCPs → GrantTap → Authenticate**. Complete
|
|
11
|
+
authorization and any one-time QR pairing on `https://granttap.com/connect`.
|
|
12
|
+
3. Run `granttap status`, then verify the GrantTap MCP tools respond in Cursor.
|
|
14
13
|
5. For a test question, send the same complete prompt to Cursor and GrantTap
|
|
15
14
|
under one exact correlation. Accept only the first answer correlated to that
|
|
16
15
|
prompt; never reuse a response from another chat or request.
|
|
17
16
|
|
|
18
17
|
Do not replace the plugin's HTTP MCP entry with stdio: Cursor exposes
|
|
19
|
-
**
|
|
18
|
+
**Authenticate** only for HTTP/SSE MCP transports.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: granttap-connect
|
|
3
|
-
description: Authorize Cursor
|
|
3
|
+
description: Authorize Cursor through granttap.com/connect, pair by QR, or reconnect GrantTap. Use when the user asks to connect, pair, show a QR, reconnect, or repair Cursor authentication.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# GrantTap connect
|
|
@@ -9,7 +9,7 @@ Connect Cursor to the GrantTap phone app without weakening chat isolation.
|
|
|
9
9
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
|
-
### 1. Configure Cursor
|
|
12
|
+
### 1. Configure Cursor authentication
|
|
13
13
|
|
|
14
14
|
Run:
|
|
15
15
|
|
|
@@ -19,7 +19,8 @@ granttap setup
|
|
|
19
19
|
|
|
20
20
|
This installs and verifies the persistent loopback OAuth/MCP service before it
|
|
21
21
|
changes GrantTap's entry in `~/.cursor/mcp.json`. Then direct the user to
|
|
22
|
-
**Cursor
|
|
22
|
+
**Cursor Customize → MCPs → GrantTap → Authenticate**. Cursor opens
|
|
23
|
+
`https://granttap.com/connect` for authorization.
|
|
23
24
|
|
|
24
25
|
The plugin endpoint must remain HTTP:
|
|
25
26
|
|
|
@@ -35,19 +36,15 @@ The plugin endpoint must remain HTTP:
|
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
Do not replace it with a stdio `command`/`args` entry; Cursor does not expose
|
|
38
|
-
its native **
|
|
39
|
+
its native **Authenticate** action for stdio MCP servers.
|
|
39
40
|
|
|
40
|
-
### 2. Pair the phone
|
|
41
|
+
### 2. Pair the phone on the website
|
|
41
42
|
|
|
42
|
-
If
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Show the resulting one-time QR in chat. Preserve the short manual code as a
|
|
49
|
-
fallback when the camera or QR rendering is unavailable. The local OAuth
|
|
50
|
-
consent page can offer the same pairing choices when Authorize starts first.
|
|
43
|
+
If this computer has no pairing, `granttap.com/connect` shows a one-time QR and
|
|
44
|
+
manual-code fallback. If it is already paired, the website reuses that pairing
|
|
45
|
+
without replacing its keys. A new QR for another phone requires the user's
|
|
46
|
+
explicit reconnect confirmation on the website. Do not present localhost as
|
|
47
|
+
the user-facing settings page.
|
|
51
48
|
|
|
52
49
|
### 3. Install policy hooks
|
|
53
50
|
|
|
@@ -71,8 +68,8 @@ granttap status
|
|
|
71
68
|
Confirm the loopback HTTP service and exact Cursor MCP entry are healthy,
|
|
72
69
|
pairing is present, and required hooks are ready. `granttap status` cannot prove
|
|
73
70
|
that Cursor retained an OAuth grant, so also confirm the MCP tools respond after
|
|
74
|
-
**
|
|
75
|
-
Cursor window and retry **
|
|
71
|
+
**Authenticate**. If Cursor loaded the MCP configuration beforehand, reload the
|
|
72
|
+
Cursor window and retry **Authenticate**.
|
|
76
73
|
|
|
77
74
|
For any interactive test, send the same complete prompt to Cursor and GrantTap
|
|
78
75
|
under one exact correlation. The first answer carrying that exact correlation
|
|
@@ -86,4 +83,5 @@ correlation.
|
|
|
86
83
|
- Run `granttap cursor repair` if `granttap status` reports an unhealthy OAuth
|
|
87
84
|
service.
|
|
88
85
|
- Re-run `granttap setup` if the policy hooks or background helper are missing.
|
|
89
|
-
-
|
|
86
|
+
- If the website cannot reach the local helper, check Chrome's local-network
|
|
87
|
+
permission for `granttap.com` and the background helper with `granttap status`.
|
package/docs/cursor-authorize.md
CHANGED
|
@@ -8,8 +8,8 @@ granttap setup
|
|
|
8
8
|
|
|
9
9
|
When Cursor is installed, setup preserves unrelated MCP entries, installs the
|
|
10
10
|
GrantTap shell and MCP policy hooks, configures the loopback HTTP MCP endpoint,
|
|
11
|
-
and installs its persistent per-user service. Then open **Cursor
|
|
12
|
-
→ GrantTap →
|
|
11
|
+
and installs its persistent per-user service. Then open **Cursor Customize →
|
|
12
|
+
MCPs → GrantTap → Authenticate** to complete authorization on granttap.com/connect.
|
|
13
13
|
|
|
14
14
|
The service binds only to loopback. Its OAuth token does not replace GrantTap's
|
|
15
15
|
end-to-end encrypted computer pairing.
|
package/package.json
CHANGED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { inspectAgentIntegrations } from "../../../bridge/src/install";
|
|
2
|
-
|
|
3
|
-
type ConsentOptions = {
|
|
4
|
-
pendingId: string;
|
|
5
|
-
paired: boolean;
|
|
6
|
-
clientName: string;
|
|
7
|
-
redirectUri: string;
|
|
8
|
-
scopes: string[];
|
|
9
|
-
resource: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
type IntegrationRow = {
|
|
13
|
-
id: "claude" | "codex";
|
|
14
|
-
label: string;
|
|
15
|
-
status: "connected" | "action_required" | "not_configured";
|
|
16
|
-
detail: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function integrationRows(): IntegrationRow[] {
|
|
20
|
-
return inspectAgentIntegrations()
|
|
21
|
-
.filter((integration): integration is typeof integration & { agent: "claude" | "codex" } =>
|
|
22
|
-
integration.agent === "claude" || integration.agent === "codex")
|
|
23
|
-
.map((integration) => ({
|
|
24
|
-
id: integration.agent,
|
|
25
|
-
label: integration.agent === "claude" ? "Claude Code" : "Codex",
|
|
26
|
-
status: integration.agent === "codex" && integration.hookConfigured
|
|
27
|
-
? "action_required"
|
|
28
|
-
: integration.hookConfigured ? "connected" : integration.installed ? "action_required" : "not_configured",
|
|
29
|
-
detail: integration.agent === "codex" && integration.hookConfigured
|
|
30
|
-
? "Both hooks installed. Open /hooks, review and trust both GrantTap hooks, then restart Codex."
|
|
31
|
-
: integration.hookConfigured ? "Approval hook installed."
|
|
32
|
-
: integration.installed ? "Run granttap setup to install the approval hook."
|
|
33
|
-
: "Agent was not found on this Mac.",
|
|
34
|
-
}));
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function escapeHtml(value: string): string {
|
|
38
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">")
|
|
39
|
-
.replaceAll('"', """).replaceAll("'", "'");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function statusCards(paired: boolean, clientName: string): string {
|
|
43
|
-
const rows = [
|
|
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: "client", label: `${clientName} MCP access`, status: "action_required", detail: "Review this local client and approve access below." },
|
|
46
|
-
...integrationRows(),
|
|
47
|
-
];
|
|
48
|
-
return rows.map((row) => {
|
|
49
|
-
const label = row.status === "connected" ? "Connected" : row.status === "action_required" ? "Action required" : "Not configured";
|
|
50
|
-
return `<div class="provider"><div><strong>${escapeHtml(row.label)}</strong><small>${escapeHtml(row.detail)}</small></div><span id="status-${row.id}" class="chip ${row.status}">${label}</span></div>`;
|
|
51
|
-
}).join("");
|
|
52
|
-
}
|
|
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
|
-
|
|
131
|
-
export function consentHtml(opts: ConsentOptions): string {
|
|
132
|
-
const { pendingId, paired, clientName, redirectUri, scopes, resource } = opts;
|
|
133
|
-
return `<!DOCTYPE html>
|
|
134
|
-
<html lang="en"><head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /><title>Authorize GrantTap</title>
|
|
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>
|
|
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>`;
|
|
138
|
-
}
|