granttap-mcp 0.8.14 → 0.8.16
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 -1
- package/apps/bridge/src/bin/setup.ts +5 -1
- package/apps/bridge/src/install.ts +10 -1
- package/apps/bridge/src/windows-service.ts +135 -0
- package/apps/mcp/src/bin/authorize.ts +3 -4
- package/apps/mcp/src/http-service/common.ts +1 -0
- package/apps/mcp/src/http-service/installer.ts +8 -1
- package/apps/mcp/src/http-service/snapshot.ts +15 -0
- package/bin/granttap-mcp.mjs +2 -2
- package/cursor-plugin/.cursor-plugin/marketplace.json +1 -1
- package/cursor-plugin/.cursor-plugin/plugin.json +1 -1
- package/cursor-plugin/README.md +20 -9
- package/cursor-plugin/assets/logo.svg +4 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -223,7 +223,8 @@ grok plugin marketplace add sergii-ziborov/granttap-mcp
|
|
|
223
223
|
grok plugin install granttap --trust
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
Import `sergii-ziborov/granttap-mcp` as a private Git plugin in Cursor. A public
|
|
227
|
+
listing requires an open-source plugin license and Cursor review. Reload Cursor, then open
|
|
227
228
|
**Cursor Customize → MCPs → GrantTap → Authenticate**. Complete authorization
|
|
228
229
|
and any one-time QR pairing on `https://granttap.com/connect`.
|
|
229
230
|
|
|
@@ -13,7 +13,7 @@ import { createOneTimePairing, DEFAULT_RELAY, PAIRING_CODE_TTL_MINUTES } from ".
|
|
|
13
13
|
import { declareEngine } from "../engine/engine-declaration";
|
|
14
14
|
import { isMachineConfigured } from "../../../mcp/src/pairing-status";
|
|
15
15
|
import { installCursorHttpConfig } from "../../../mcp/src/cursor-config";
|
|
16
|
-
import { installHttpMcpService } from "../../../mcp/src/http-service";
|
|
16
|
+
import { installHttpMcpService, waitForHttpMcpHealth } from "../../../mcp/src/http-service";
|
|
17
17
|
|
|
18
18
|
function state(result: InstallResult): string {
|
|
19
19
|
return result.status === "manual" ? "Needs attention" : "Ready";
|
|
@@ -67,6 +67,10 @@ async function main(): Promise<void> {
|
|
|
67
67
|
const mcpClientInstalled = before.agents.some((item) => item.installed);
|
|
68
68
|
if (before.cursor.installed || mcpClientInstalled) {
|
|
69
69
|
httpService = installHttpMcpService();
|
|
70
|
+
if (process.platform === "win32" && httpService.status !== "manual"
|
|
71
|
+
&& !await waitForHttpMcpHealth(undefined, 15_000)) {
|
|
72
|
+
httpService = { status: "manual", detail: "Windows background service did not become healthy." };
|
|
73
|
+
}
|
|
70
74
|
if (before.cursor.installed && httpService.status !== "manual") {
|
|
71
75
|
cursorConfig = installCursorHttpConfig();
|
|
72
76
|
}
|
|
@@ -28,6 +28,7 @@ import { spawnSync } from "node:child_process";
|
|
|
28
28
|
import type { CodingAgent } from "../../../packages/protocol/schema";
|
|
29
29
|
import { refusesLiveLaunchd } from "./launchd-safety";
|
|
30
30
|
import { configDir, loadRuntimeConfig, verifiableEngine } from "./config";
|
|
31
|
+
import { inspectWindowsTask, installWindowsTask } from "./windows-service";
|
|
31
32
|
|
|
32
33
|
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
33
34
|
|
|
@@ -287,6 +288,7 @@ export type MonitorIntegrationStatus = {
|
|
|
287
288
|
|
|
288
289
|
/** Read-only status check. It never installs, reloads, or repairs the helper. */
|
|
289
290
|
export function inspectMonitorHelper(): MonitorIntegrationStatus {
|
|
291
|
+
if (process.platform === "win32") return inspectWindowsTask("monitor");
|
|
290
292
|
if (process.platform !== "darwin") return { configured: false, running: false };
|
|
291
293
|
const agentsDir = process.env.GRANTTAP_LAUNCH_AGENTS_DIR
|
|
292
294
|
?? join(homedir(), "Library", "LaunchAgents");
|
|
@@ -340,7 +342,8 @@ function xml(value: string): string {
|
|
|
340
342
|
}
|
|
341
343
|
|
|
342
344
|
export function isCursorHelperNode(nodePath: string): boolean {
|
|
343
|
-
return
|
|
345
|
+
return /Cursor\.app|[\\/]Cursor[\\/].*[\\/]helpers[\\/]node/i.test(nodePath)
|
|
346
|
+
|| nodePath.includes("/helpers/node");
|
|
344
347
|
}
|
|
345
348
|
|
|
346
349
|
/** True when a plist uses the caller's explicit development monitor pin. */
|
|
@@ -407,6 +410,12 @@ function engineEnvironment(): string[] {
|
|
|
407
410
|
}
|
|
408
411
|
|
|
409
412
|
export function installMonitorHelper(): InstallResult {
|
|
413
|
+
if (process.platform === "win32") {
|
|
414
|
+
const node = resolveMonitorNodeBin();
|
|
415
|
+
return node && !isCursorHelperNode(node)
|
|
416
|
+
? installWindowsTask("monitor", node, join(repoRoot, "bin", "granttap-mcp.mjs"))
|
|
417
|
+
: { status: "manual", detail: "A stable Node.js 20+ installation is required for Windows background sync." };
|
|
418
|
+
}
|
|
410
419
|
if (process.platform !== "darwin") {
|
|
411
420
|
return { status: "manual", detail: "background task sync currently requires macOS" };
|
|
412
421
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/** Per-user Windows Task Scheduler jobs for the loopback MCP and phone monitor. */
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { configDir } from "./config";
|
|
6
|
+
import type { InstallResult } from "./install";
|
|
7
|
+
|
|
8
|
+
export type WindowsJob = "http" | "monitor";
|
|
9
|
+
const names: Record<WindowsJob, string> = {
|
|
10
|
+
http: "GrantTap MCP HTTP",
|
|
11
|
+
monitor: "GrantTap Phone Monitor",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function windowsTaskName(job: WindowsJob): string { return names[job]; }
|
|
15
|
+
|
|
16
|
+
function xml(value: string): string {
|
|
17
|
+
return value.replaceAll("&", "&").replaceAll("<", "<")
|
|
18
|
+
.replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function windowsTaskDefinition(job: WindowsJob, node: string, launcher: string, owner = "TestUser", version = "0.0.0"): string {
|
|
22
|
+
const date = new Date(Date.now() - 60_000);
|
|
23
|
+
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
|
|
24
|
+
const start = local.toISOString().slice(0, 19);
|
|
25
|
+
const args = `"${launcher}" internal ${job === "http" ? "serve" : "monitor"}`;
|
|
26
|
+
return [
|
|
27
|
+
'<?xml version="1.0" encoding="UTF-16"?>',
|
|
28
|
+
'<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">',
|
|
29
|
+
`<RegistrationInfo><Description>GrantTap ${xml(version)} ${job}</Description></RegistrationInfo>`,
|
|
30
|
+
"<Triggers><TimeTrigger>", `<StartBoundary>${start}</StartBoundary>`,
|
|
31
|
+
"<Enabled>true</Enabled><Repetition><Interval>PT1M</Interval></Repetition>",
|
|
32
|
+
"</TimeTrigger></Triggers>",
|
|
33
|
+
`<Principals><Principal id="Author"><UserId>${xml(owner)}</UserId>`,
|
|
34
|
+
"<LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>",
|
|
35
|
+
"<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>",
|
|
36
|
+
"<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>",
|
|
37
|
+
"<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>",
|
|
38
|
+
"<StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit>",
|
|
39
|
+
"<RestartOnFailure><Interval>PT1M</Interval><Count>3</Count></RestartOnFailure>",
|
|
40
|
+
"</Settings>",
|
|
41
|
+
`<Actions Context="Author"><Exec><Command>${xml(node)}</Command>`,
|
|
42
|
+
`<Arguments>${xml(args)}</Arguments><WorkingDirectory>${xml(dirname(launcher))}</WorkingDirectory>`,
|
|
43
|
+
"</Exec></Actions></Task>", "",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function taskXml(job: WindowsJob): string | null {
|
|
48
|
+
const result = spawnSync("schtasks.exe", ["/Query", "/TN", names[job], "/XML"], { windowsHide: true });
|
|
49
|
+
if (result.status !== 0) return null;
|
|
50
|
+
const output = result.stdout;
|
|
51
|
+
return output[0] === 0xff && output[1] === 0xfe || output.includes(0)
|
|
52
|
+
? output.toString("utf16le").replace(/^\uFEFF/, "")
|
|
53
|
+
: output.toString("utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function owned(job: WindowsJob, data: string): boolean {
|
|
57
|
+
return /<Command>[^<]*node\.exe<\/Command>/i.test(data)
|
|
58
|
+
&& /granttap-mcp\.mjs(?:"|\")? internal (?:serve|monitor)/i.test(data)
|
|
59
|
+
&& data.includes(`internal ${job === "http" ? "serve" : "monitor"}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function identity(): string | null {
|
|
63
|
+
const result = spawnSync("whoami.exe", [], { encoding: "utf8", windowsHide: true });
|
|
64
|
+
return result.status === 0 ? result.stdout.trim() : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function taskFile(job: WindowsJob): string { return join(configDir(), `windows-${job}-task.xml`); }
|
|
68
|
+
|
|
69
|
+
export function inspectWindowsTask(job: WindowsJob): { configured: boolean; running: boolean } {
|
|
70
|
+
const data = taskXml(job);
|
|
71
|
+
if (!data || !owned(job, data)) return { configured: false, running: false };
|
|
72
|
+
const state = spawnSync("powershell.exe", [
|
|
73
|
+
"-NoProfile", "-NonInteractive", "-Command",
|
|
74
|
+
`(Get-ScheduledTask -TaskName '${names[job]}').State.ToString()`,
|
|
75
|
+
], { encoding: "utf8", windowsHide: true });
|
|
76
|
+
return { configured: true, running: state.status === 0 && state.stdout.trim() === "Running" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installWindowsTask(job: WindowsJob, node: string, launcher: string, forceReload = false): InstallResult {
|
|
80
|
+
if (!existsSync(node) || !existsSync(launcher)) {
|
|
81
|
+
return { status: "manual", detail: "Stable Node.js and granttap-mcp installation are required on Windows." };
|
|
82
|
+
}
|
|
83
|
+
const previous = taskXml(job);
|
|
84
|
+
if (previous && !owned(job, previous)) {
|
|
85
|
+
return { status: "manual", detail: `The existing ${names[job]} task is not owned by GrantTap.` };
|
|
86
|
+
}
|
|
87
|
+
const owner = identity();
|
|
88
|
+
if (!owner) return { status: "manual", detail: "Could not identify the current Windows user." };
|
|
89
|
+
let version: string;
|
|
90
|
+
try {
|
|
91
|
+
version = (JSON.parse(readFileSync(join(dirname(launcher), "..", "package.json"), "utf8")) as { version: string }).version;
|
|
92
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error("invalid version");
|
|
93
|
+
} catch {
|
|
94
|
+
return { status: "manual", detail: "Installed GrantTap package version could not be read." };
|
|
95
|
+
}
|
|
96
|
+
const path = taskFile(job);
|
|
97
|
+
const definition = windowsTaskDefinition(job, node, launcher, owner, version);
|
|
98
|
+
const previousCommand = previous?.match(/<Command>([^<]*)<\/Command>/)?.[1];
|
|
99
|
+
const previousArgs = previous?.match(/<Arguments>([^<]*)<\/Arguments>/)?.[1];
|
|
100
|
+
const normalizedArgs = previousArgs?.replaceAll(""", '"').replaceAll("&", "&");
|
|
101
|
+
const already = previousCommand === xml(node)
|
|
102
|
+
&& normalizedArgs === `"${launcher}" internal ${job === "http" ? "serve" : "monitor"}`
|
|
103
|
+
&& previous?.includes("<Interval>PT1M</Interval>") === true
|
|
104
|
+
&& previous.includes(`<Description>GrantTap ${version} ${job}</Description>`);
|
|
105
|
+
if (already && !forceReload && inspectWindowsTask(job).running) return { status: "already", detail: names[job] };
|
|
106
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
107
|
+
// Task Scheduler reads an XML file; keep it private to this user's config directory.
|
|
108
|
+
writeFileSync(path, `\uFEFF${definition}`, { encoding: "utf16le", mode: 0o600 });
|
|
109
|
+
const created = spawnSync("schtasks.exe", ["/Create", "/TN", names[job], "/XML", path, "/F"], {
|
|
110
|
+
encoding: "utf8", windowsHide: true,
|
|
111
|
+
});
|
|
112
|
+
unlinkSync(path);
|
|
113
|
+
if (created.status !== 0) return { status: "manual", detail: `${names[job]} could not be registered for this Windows user.` };
|
|
114
|
+
if (previous && (forceReload || !already)) {
|
|
115
|
+
spawnSync("schtasks.exe", ["/End", "/TN", names[job]], { windowsHide: true });
|
|
116
|
+
}
|
|
117
|
+
const started = spawnSync("schtasks.exe", ["/Run", "/TN", names[job]], { encoding: "utf8", windowsHide: true });
|
|
118
|
+
return started.status === 0
|
|
119
|
+
? { status: already ? "already" : "installed", detail: names[job] }
|
|
120
|
+
: { status: "manual", detail: `${names[job]} was registered but did not start.` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function snapshotWindowsTask(job: WindowsJob): string | null { return taskXml(job); }
|
|
124
|
+
|
|
125
|
+
export function restoreWindowsTask(job: WindowsJob, previous: string | null): boolean {
|
|
126
|
+
const current = taskXml(job);
|
|
127
|
+
if (!current || !owned(job, current) || (previous && !owned(job, previous))) return false;
|
|
128
|
+
spawnSync("schtasks.exe", ["/End", "/TN", names[job]], { windowsHide: true });
|
|
129
|
+
if (!previous) return spawnSync("schtasks.exe", ["/Delete", "/TN", names[job], "/F"], { windowsHide: true }).status === 0;
|
|
130
|
+
const path = taskFile(job);
|
|
131
|
+
writeFileSync(path, `\uFEFF${previous}`, { encoding: "utf16le", mode: 0o600 });
|
|
132
|
+
const restored = spawnSync("schtasks.exe", ["/Create", "/TN", names[job], "/XML", path, "/F"], { windowsHide: true });
|
|
133
|
+
unlinkSync(path);
|
|
134
|
+
return restored.status === 0 && spawnSync("schtasks.exe", ["/Run", "/TN", names[job]], { windowsHide: true }).status === 0;
|
|
135
|
+
}
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
waitForHttpMcpHealth,
|
|
14
14
|
} from "../http-service";
|
|
15
15
|
import type { InstallResult } from "../../../bridge/src/install";
|
|
16
|
-
import { existsSync } from "node:fs";
|
|
17
16
|
|
|
18
17
|
async function authorize(): Promise<void> {
|
|
19
18
|
const preflight = validateCursorHttpConfig();
|
|
@@ -33,7 +32,7 @@ async function authorize(): Promise<void> {
|
|
|
33
32
|
if (alreadyHealthy) {
|
|
34
33
|
if (before.configured && before.running) {
|
|
35
34
|
service = { status: "already", detail: httpMcpLaunchAgentPath() };
|
|
36
|
-
} else if (
|
|
35
|
+
} else if (serviceBefore.exists) {
|
|
37
36
|
repairAttempted = true;
|
|
38
37
|
service = installHttpMcpService({ forceReload: true });
|
|
39
38
|
} else {
|
|
@@ -70,9 +69,9 @@ async function authorize(): Promise<void> {
|
|
|
70
69
|
[
|
|
71
70
|
`[granttap-mcp] Cursor · Configured — ${cursor.detail}`,
|
|
72
71
|
`[granttap-mcp] Persistent local OAuth is healthy at ${mcpUrl}`,
|
|
73
|
-
`[granttap-mcp]
|
|
72
|
+
`[granttap-mcp] Background service: ${service.status} (${service.detail})`,
|
|
74
73
|
"[granttap-mcp] Open Cursor Customize → MCPs → GrantTap → Authenticate. The browser continues at granttap.com/connect.",
|
|
75
|
-
"[granttap-mcp] If this
|
|
74
|
+
"[granttap-mcp] If this computer is not paired, granttap.com/connect shows a QR and manual token.",
|
|
76
75
|
"",
|
|
77
76
|
].join("\n"),
|
|
78
77
|
);
|
|
@@ -7,6 +7,7 @@ export const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "
|
|
|
7
7
|
export const HTTP_SERVICE_LABEL = "com.granttap.mcp-http";
|
|
8
8
|
|
|
9
9
|
export function httpMcpLaunchAgentPath(): string {
|
|
10
|
+
if (process.platform === "win32") return "GrantTap MCP HTTP";
|
|
10
11
|
const agentsDir = process.env.GRANTTAP_LAUNCH_AGENTS_DIR
|
|
11
12
|
?? join(homedir(), "Library", "LaunchAgents");
|
|
12
13
|
return join(agentsDir, `${HTTP_SERVICE_LABEL}.plist`);
|
|
@@ -5,13 +5,14 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import { isCursorHelperNode, resolveMonitorNodeBin, type InstallResult } from "../../../bridge/src/install";
|
|
7
7
|
import { configDir, normalizeRelayUrl } from "../../../bridge/src/config";
|
|
8
|
+
import { installWindowsTask } from "../../../bridge/src/windows-service";
|
|
8
9
|
import { configuredCursorHttpMcpUrl } from "../cursor-config";
|
|
9
10
|
import { HTTP_SERVICE_LABEL, httpMcpLaunchAgentPath, packageRoot, xml } from "./common";
|
|
10
11
|
import { inspectHttpMcpService, restoreHttpMcpServiceAfterFailure, snapshotHttpMcpService } from "./snapshot";
|
|
11
12
|
|
|
12
13
|
/** Install/repair and load a per-user, loopback-only OAuth MCP daemon. */
|
|
13
14
|
export function installHttpMcpService(options: { forceReload?: boolean } = {}): InstallResult {
|
|
14
|
-
if (process.platform !== "darwin") return macosOnly();
|
|
15
|
+
if (process.platform !== "darwin" && process.platform !== "win32") return macosOnly();
|
|
15
16
|
const mcpUrl = configuredMcpUrl();
|
|
16
17
|
if (mcpUrl instanceof Error) return { status: "manual", detail: mcpUrl.message };
|
|
17
18
|
const nodeBin = safeNodeBin();
|
|
@@ -19,6 +20,12 @@ export function installHttpMcpService(options: { forceReload?: boolean } = {}):
|
|
|
19
20
|
const executable = join(packageRoot, "bin", "granttap-mcp.mjs");
|
|
20
21
|
const installation = validateInstallation(nodeBin, executable);
|
|
21
22
|
if (installation) return installation;
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
if (mcpUrl.href !== "http://127.0.0.1:17342/mcp") {
|
|
25
|
+
return { status: "manual", detail: "Windows background service currently uses the default loopback endpoint." };
|
|
26
|
+
}
|
|
27
|
+
return installWindowsTask("http", nodeBin, executable, options.forceReload);
|
|
28
|
+
}
|
|
22
29
|
const path = httpMcpLaunchAgentPath();
|
|
23
30
|
const logsDir = join(configDir(), "logs");
|
|
24
31
|
const plist = createPlist(nodeBin, executable, mcpUrl, logsDir);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { dirname } from "node:path";
|
|
14
|
+
import { inspectWindowsTask, restoreWindowsTask, snapshotWindowsTask, windowsTaskName } from "../../../bridge/src/windows-service";
|
|
14
15
|
import {
|
|
15
16
|
httpMcpLaunchAgentPath,
|
|
16
17
|
isConfiguredHttpService,
|
|
@@ -32,6 +33,15 @@ export type HttpMcpServiceSnapshot = {
|
|
|
32
33
|
|
|
33
34
|
/** Exact plist bytes/mode and loaded state for transactional repair rollback. */
|
|
34
35
|
export function snapshotHttpMcpService(): HttpMcpServiceSnapshot {
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
const task = snapshotWindowsTask("http");
|
|
38
|
+
const state = inspectWindowsTask("http");
|
|
39
|
+
return {
|
|
40
|
+
path: windowsTaskName("http"), exists: task !== null,
|
|
41
|
+
plist: task === null ? null : Buffer.from(task), mode: 0o600,
|
|
42
|
+
owned: state.configured, configured: state.configured, running: state.running,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
35
45
|
const path = httpMcpLaunchAgentPath();
|
|
36
46
|
if (!existsSync(path)) return missingSnapshot(path);
|
|
37
47
|
let plist: Buffer;
|
|
@@ -65,6 +75,7 @@ function unreadableSnapshot(path: string): HttpMcpServiceSnapshot {
|
|
|
65
75
|
|
|
66
76
|
/** Read-only plist/process inspection. Health is probed separately. */
|
|
67
77
|
export function inspectHttpMcpService(): HttpMcpServiceStatus {
|
|
78
|
+
if (process.platform === "win32") return inspectWindowsTask("http");
|
|
68
79
|
if (process.platform !== "darwin") return { configured: false, running: false };
|
|
69
80
|
const snapshot = snapshotHttpMcpService();
|
|
70
81
|
return { configured: snapshot.configured, running: snapshot.configured && snapshot.running };
|
|
@@ -72,6 +83,10 @@ export function inspectHttpMcpService(): HttpMcpServiceStatus {
|
|
|
72
83
|
|
|
73
84
|
/** Restore a failed repair, deleting the service only when this attempt created it. */
|
|
74
85
|
export function restoreHttpMcpServiceAfterFailure(before: HttpMcpServiceSnapshot): boolean {
|
|
86
|
+
if (process.platform === "win32") {
|
|
87
|
+
if (before.exists && !before.owned) return false;
|
|
88
|
+
return restoreWindowsTask("http", before.plist?.toString("utf8") ?? null);
|
|
89
|
+
}
|
|
75
90
|
if (before.exists && !before.owned) return false;
|
|
76
91
|
const current = currentOwnedPlist(before.path);
|
|
77
92
|
if (current === undefined) return false;
|
package/bin/granttap-mcp.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
5
|
+
import { delimiter, dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
|
|
8
8
|
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -107,7 +107,7 @@ const child = spawn(
|
|
|
107
107
|
cwd: root,
|
|
108
108
|
env: {
|
|
109
109
|
...process.env,
|
|
110
|
-
NODE_PATH: [join(root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(
|
|
110
|
+
NODE_PATH: [join(root, "node_modules"), process.env.NODE_PATH].filter(Boolean).join(delimiter),
|
|
111
111
|
},
|
|
112
112
|
},
|
|
113
113
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "granttap",
|
|
3
3
|
"displayName": "GrantTap",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
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",
|
package/cursor-plugin/README.md
CHANGED
|
@@ -6,8 +6,7 @@ each phone response tied to the exact originating chat and prompt.
|
|
|
6
6
|
|
|
7
7
|
## Connect Cursor
|
|
8
8
|
|
|
9
|
-
Install the supported CLI
|
|
10
|
-
then run:
|
|
9
|
+
Install the supported CLI on this computer, then run:
|
|
11
10
|
|
|
12
11
|
```bash
|
|
13
12
|
npm install -g granttap-mcp
|
|
@@ -25,19 +24,31 @@ The commands have separate jobs:
|
|
|
25
24
|
|
|
26
25
|
After `granttap setup`, open **Cursor Customize → MCPs → GrantTap** and choose
|
|
27
26
|
**Authenticate**. The browser opens granttap.com/connect to authorize Cursor.
|
|
28
|
-
If
|
|
27
|
+
If this computer is not paired, the website shows a one-time QR and manual-code
|
|
29
28
|
fallback; a saved pairing is reused unless you explicitly confirm reconnect.
|
|
30
29
|
|
|
30
|
+
On Windows, `granttap setup` registers the loopback MCP and phone monitor as
|
|
31
|
+
least-privileged jobs under your signed-in account. Install Node.js 20+ and the
|
|
32
|
+
current `granttap-mcp` package on that same Windows computer; the plugin alone
|
|
33
|
+
cannot start the local server. The jobs restart after a crash and keep running
|
|
34
|
+
when Cursor closes. Run `granttap status` to confirm readiness before choosing
|
|
35
|
+
**Authenticate** in Cursor.
|
|
36
|
+
|
|
31
37
|
The OAuth service listens only at `http://127.0.0.1:17342/mcp`. Cursor cannot
|
|
32
38
|
show **Authenticate** for a stdio (`command`/`args`) MCP entry, so do not replace the
|
|
33
39
|
plugin's HTTP configuration with stdio.
|
|
34
40
|
|
|
35
|
-
## Install
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
## Install the Cursor plugin
|
|
42
|
+
|
|
43
|
+
A public Cursor Marketplace listing requires an open-source plugin license and
|
|
44
|
+
manual review. The current commercially licensed plugin is available as a
|
|
45
|
+
private Git import: `sergii-ziborov/granttap-mcp`. Reload Cursor after import.
|
|
46
|
+
The private listing is tied to the version imported on that computer; re-import
|
|
47
|
+
it to pick up the current orange icon and plugin changes. If a public listing
|
|
48
|
+
is later approved, install **GrantTap** from Cursor's marketplace to receive
|
|
49
|
+
its reviewed updates. The plugin adds the `/connect` command, a connect
|
|
50
|
+
skill, and the exact-correlation rule; `granttap setup` configures the local
|
|
51
|
+
MCP endpoint separately.
|
|
41
52
|
|
|
42
53
|
## Exact dual-channel behavior
|
|
43
54
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"
|
|
2
|
-
<rect width="64" height="64" rx="
|
|
3
|
-
<
|
|
4
|
-
<path d="
|
|
5
|
-
<path d="M40 36c0 4.4-3.6 8-8 8h-2" stroke="#3DDC97" stroke-width="3" stroke-linecap="round"/>
|
|
6
|
-
<circle cx="32" cy="32" r="3" fill="#3DDC97"/>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GrantTap">
|
|
2
|
+
<rect width="64" height="64" rx="10" fill="#f47f42"/>
|
|
3
|
+
<path d="M11 31.5 24.5 45 42 19" fill="none" stroke="#1d130e" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M45.5 29v10M52 25v18M58.5 29v10" fill="none" stroke="#1d130e" stroke-width="5" stroke-linecap="round"/>
|
|
7
5
|
</svg>
|