poke-gate 0.3.1 → 0.3.3
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/.github/copilot-desktop.yml +8 -0
- package/clients/Poke macOS Gate/Poke macOS Gate/GateService.swift +6 -2
- package/clients/Poke macOS Gate/Poke macOS Gate.xcodeproj/project.pbxproj +2 -2
- package/package.json +1 -1
- package/src/app.js +29 -18
- package/src/mcp-server.js +36 -4
- package/src/poke-auth.js +25 -0
- package/src/take-screenshot.js +3 -9
- package/src/tunnel.js +8 -26
- package/src/webhook.js +47 -0
- package/test/mcp-server-transport.test.js +70 -0
- package/test/poke-auth.test.js +16 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
scripts:
|
|
2
|
+
- name: xcode-build
|
|
3
|
+
command: cd "clients/Poke macOS Gate" && xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build | xcpretty || xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build
|
|
4
|
+
- name: xcode-run-simulator
|
|
5
|
+
command: cd "clients/Poke macOS Gate" && xcodebuild -scheme "Poke macOS Gate" -configuration Debug -destination 'platform=macOS' build && APP_PATH=$(find ~/Library/Developer/Xcode/DerivedData -name "Poke macOS Gate.app" -type d 2>/dev/null | head -1) && [ -n "$APP_PATH" ] && open "$APP_PATH"
|
|
6
|
+
automation:
|
|
7
|
+
auto_pr_review: false
|
|
8
|
+
auto_issue_session: true
|
|
@@ -507,13 +507,17 @@ class GateService: ObservableObject {
|
|
|
507
507
|
|
|
508
508
|
proc.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
|
509
509
|
proc.arguments = ["-c", "\(npxBin) -y poke-gate@latest --verbose"]
|
|
510
|
-
|
|
510
|
+
var environment = ProcessInfo.processInfo.environment.merging(
|
|
511
511
|
[
|
|
512
512
|
"PATH": fullPath,
|
|
513
513
|
"POKE_GATE_PERMISSION_MODE": permissionMode.rawValue,
|
|
514
514
|
],
|
|
515
515
|
uniquingKeysWith: { _, new in new }
|
|
516
516
|
)
|
|
517
|
+
if let token = resolveToken() {
|
|
518
|
+
environment["POKE_API_KEY"] = token
|
|
519
|
+
}
|
|
520
|
+
proc.environment = environment
|
|
517
521
|
proc.standardOutput = pipe
|
|
518
522
|
proc.standardError = pipe
|
|
519
523
|
proc.currentDirectoryURL = FileManager.default.homeDirectoryForCurrentUser
|
|
@@ -556,7 +560,7 @@ class GateService: ObservableObject {
|
|
|
556
560
|
private func killOrphanedProcesses() {
|
|
557
561
|
let task = Process()
|
|
558
562
|
task.executableURL = URL(fileURLWithPath: "/usr/bin/pkill")
|
|
559
|
-
task.arguments = ["-f", "node.*poke-gate.*app\\.js"]
|
|
563
|
+
task.arguments = ["-f", "node .*((poke-gate.*app\\.js)|(\\.bin/poke-gate))"]
|
|
560
564
|
try? task.run()
|
|
561
565
|
task.waitUntilExit()
|
|
562
566
|
}
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"@executable_path/../Frameworks",
|
|
287
287
|
);
|
|
288
288
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
|
289
|
-
MARKETING_VERSION = 0.3.
|
|
289
|
+
MARKETING_VERSION = 0.3.2;
|
|
290
290
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
|
|
291
291
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
292
292
|
REGISTER_APP_GROUPS = YES;
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
"@executable_path/../Frameworks",
|
|
323
323
|
);
|
|
324
324
|
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
|
325
|
-
MARKETING_VERSION = 0.3.
|
|
325
|
+
MARKETING_VERSION = 0.3.2;
|
|
326
326
|
PRODUCT_BUNDLE_IDENTIFIER = "dev.fka.Poke-macOS-Gate";
|
|
327
327
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
|
328
328
|
REGISTER_APP_GROUPS = YES;
|
package/package.json
CHANGED
package/src/app.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { startMcpServer, enableLogging, getPermissionMode } from "./mcp-server.js";
|
|
2
2
|
import { startTunnel } from "./tunnel.js";
|
|
3
3
|
import { startAgentScheduler, stopAgentScheduler } from "./agents.js";
|
|
4
|
-
import {
|
|
4
|
+
import { sendToWebhook } from "./webhook.js";
|
|
5
|
+
import { ensurePokeAuthenticated } from "./poke-auth.js";
|
|
5
6
|
import { execSync } from "node:child_process";
|
|
6
7
|
|
|
7
8
|
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
@@ -11,8 +12,28 @@ function killExistingInstances() {
|
|
|
11
12
|
const myPid = process.pid;
|
|
12
13
|
const ppid = process.ppid;
|
|
13
14
|
try {
|
|
14
|
-
const out = execSync("
|
|
15
|
-
const pids = out
|
|
15
|
+
const out = execSync("ps -axo pid=,ppid=,command=", { encoding: "utf-8" }).trim();
|
|
16
|
+
const pids = out
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) => {
|
|
19
|
+
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(.+)$/);
|
|
20
|
+
if (!match) return null;
|
|
21
|
+
const [, pid, parentPid, command] = match;
|
|
22
|
+
return { pid: Number(pid), parentPid: Number(parentPid), command };
|
|
23
|
+
})
|
|
24
|
+
.filter((processInfo) => {
|
|
25
|
+
if (!processInfo || processInfo.pid === myPid || processInfo.pid === ppid || processInfo.parentPid === myPid) return false;
|
|
26
|
+
return (
|
|
27
|
+
processInfo.command.includes("node ") &&
|
|
28
|
+
processInfo.command.includes("poke-gate") &&
|
|
29
|
+
(
|
|
30
|
+
processInfo.command.includes("app.js") ||
|
|
31
|
+
processInfo.command.includes(".bin/poke-gate") ||
|
|
32
|
+
processInfo.command.includes("/bin/poke-gate")
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
})
|
|
36
|
+
.map(({ pid }) => pid);
|
|
16
37
|
for (const pid of pids) {
|
|
17
38
|
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
18
39
|
}
|
|
@@ -30,17 +51,7 @@ function sleep(ms) {
|
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
async function ensureAuthenticated() {
|
|
33
|
-
|
|
34
|
-
log("Signing in to Poke...");
|
|
35
|
-
await login();
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const token = getToken();
|
|
39
|
-
if (!token) {
|
|
40
|
-
throw new Error("Authentication failed: no token returned by Poke SDK.");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return token;
|
|
54
|
+
return ensurePokeAuthenticated({ onLogin: () => log("Signing in to Poke...") });
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
let currentTunnel = null;
|
|
@@ -59,6 +70,7 @@ async function connectWithRetry(mcpUrl, token) {
|
|
|
59
70
|
|
|
60
71
|
const { tunnel } = await startTunnel({
|
|
61
72
|
mcpUrl,
|
|
73
|
+
token,
|
|
62
74
|
onEvent: (type, data) => {
|
|
63
75
|
switch (type) {
|
|
64
76
|
case "connected":
|
|
@@ -67,7 +79,7 @@ async function connectWithRetry(mcpUrl, token) {
|
|
|
67
79
|
reconnectWatchdog = null;
|
|
68
80
|
log(`Tunnel connected (${data.connectionId})`);
|
|
69
81
|
log("Ready — your Poke agent can now access this machine.");
|
|
70
|
-
notifyPoke(data.connectionId
|
|
82
|
+
notifyPoke(data.connectionId);
|
|
71
83
|
startAgentScheduler();
|
|
72
84
|
break;
|
|
73
85
|
case "disconnected":
|
|
@@ -153,11 +165,10 @@ function buildAccessModeMessage(mode) {
|
|
|
153
165
|
}
|
|
154
166
|
}
|
|
155
167
|
|
|
156
|
-
async function notifyPoke(connectionId
|
|
168
|
+
async function notifyPoke(connectionId) {
|
|
157
169
|
try {
|
|
158
170
|
const mode = getPermissionMode();
|
|
159
|
-
|
|
160
|
-
await poke.sendMessage(
|
|
171
|
+
await sendToWebhook(
|
|
161
172
|
`Hey! I've connected my computer to you via Poke Gate (tunnel: ${connectionId}). ` +
|
|
162
173
|
`${buildAccessModeMessage(mode)} ` +
|
|
163
174
|
`Just use the tools whenever I ask you to do something on my computer. ` +
|
package/src/mcp-server.js
CHANGED
|
@@ -847,12 +847,29 @@ function readBody(req) {
|
|
|
847
847
|
});
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
+
function writeMcpEventStream(req, res) {
|
|
851
|
+
const sessionId = extractSessionId(req);
|
|
852
|
+
res.writeHead(200, {
|
|
853
|
+
"Content-Type": "text/event-stream",
|
|
854
|
+
"Cache-Control": "no-cache, no-transform",
|
|
855
|
+
Connection: "keep-alive",
|
|
856
|
+
"Mcp-Session-Id": sessionId,
|
|
857
|
+
});
|
|
858
|
+
res.write(": connected\n\n");
|
|
859
|
+
|
|
860
|
+
const keepAlive = setInterval(() => {
|
|
861
|
+
res.write(": keepalive\n\n");
|
|
862
|
+
}, 25_000);
|
|
863
|
+
|
|
864
|
+
req.on("close", () => clearInterval(keepAlive));
|
|
865
|
+
}
|
|
866
|
+
|
|
850
867
|
export function startMcpServer(port = 0) {
|
|
851
868
|
return new Promise((resolve, reject) => {
|
|
852
869
|
const httpServer = http.createServer(async (req, res) => {
|
|
853
870
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
854
871
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
855
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept");
|
|
872
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, Accept, X-Poke-User-Id");
|
|
856
873
|
res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
|
|
857
874
|
|
|
858
875
|
if (req.method === "OPTIONS") {
|
|
@@ -863,12 +880,27 @@ export function startMcpServer(port = 0) {
|
|
|
863
880
|
|
|
864
881
|
const url = new URL(req.url, "http://localhost");
|
|
865
882
|
|
|
883
|
+
if (url.pathname === "/mcp" && req.method === "GET") {
|
|
884
|
+
const accept = req.headers.accept || "";
|
|
885
|
+
if (accept.includes("text/event-stream")) {
|
|
886
|
+
writeMcpEventStream(req, res);
|
|
887
|
+
} else {
|
|
888
|
+
res.writeHead(405, { "Content-Type": "text/plain", Allow: "POST, OPTIONS" });
|
|
889
|
+
res.end("MCP endpoint expects POST, or GET with Accept: text/event-stream");
|
|
890
|
+
}
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
|
|
866
894
|
if (url.pathname === "/mcp" && req.method === "POST") {
|
|
867
895
|
try {
|
|
868
896
|
const body = await readBody(req);
|
|
869
897
|
const parsed = JSON.parse(body);
|
|
870
898
|
|
|
871
899
|
const sessionId = extractSessionId(req);
|
|
900
|
+
const responseHeaders = {
|
|
901
|
+
"Content-Type": "application/json",
|
|
902
|
+
"Mcp-Session-Id": sessionId,
|
|
903
|
+
};
|
|
872
904
|
|
|
873
905
|
if (Array.isArray(parsed)) {
|
|
874
906
|
const results = [];
|
|
@@ -878,17 +910,17 @@ export function startMcpServer(port = 0) {
|
|
|
878
910
|
const resolved = r instanceof Promise ? await r : r;
|
|
879
911
|
if (resolved) results.push(resolved);
|
|
880
912
|
}
|
|
881
|
-
res.writeHead(200,
|
|
913
|
+
res.writeHead(200, responseHeaders);
|
|
882
914
|
res.end(JSON.stringify(results));
|
|
883
915
|
} else {
|
|
884
916
|
const m = { ...parsed, __context: { sessionId } };
|
|
885
917
|
let result = handleJsonRpc(m);
|
|
886
918
|
if (result instanceof Promise) result = await result;
|
|
887
919
|
if (result) {
|
|
888
|
-
res.writeHead(200,
|
|
920
|
+
res.writeHead(200, responseHeaders);
|
|
889
921
|
res.end(JSON.stringify(result));
|
|
890
922
|
} else {
|
|
891
|
-
res.writeHead(204);
|
|
923
|
+
res.writeHead(204, { "Mcp-Session-Id": sessionId });
|
|
892
924
|
res.end();
|
|
893
925
|
}
|
|
894
926
|
}
|
package/src/poke-auth.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { getToken, isLoggedIn, login } from "poke";
|
|
2
|
+
|
|
3
|
+
export function resolvePokeToken(options = {}) {
|
|
4
|
+
const { env = process.env } = options;
|
|
5
|
+
const loginToken = Object.hasOwn(options, "token") ? options.token : getToken();
|
|
6
|
+
return env.POKE_API_KEY || loginToken;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getPokeAuthToken() {
|
|
10
|
+
return resolvePokeToken();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function ensurePokeAuthenticated({ onLogin } = {}) {
|
|
14
|
+
if (!getPokeAuthToken() && !isLoggedIn()) {
|
|
15
|
+
onLogin?.();
|
|
16
|
+
await login();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const token = getPokeAuthToken();
|
|
20
|
+
if (!token) {
|
|
21
|
+
throw new Error("Authentication failed: no token returned by Poke SDK.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return token;
|
|
25
|
+
}
|
package/src/take-screenshot.js
CHANGED
|
@@ -2,7 +2,8 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { tmpdir, platform } from "node:os";
|
|
5
|
-
import {
|
|
5
|
+
import { isLoggedIn, login } from "poke";
|
|
6
|
+
import { sendToWebhook } from "./webhook.js";
|
|
6
7
|
|
|
7
8
|
export async function takeScreenshot() {
|
|
8
9
|
if (platform() !== "darwin") {
|
|
@@ -15,12 +16,6 @@ export async function takeScreenshot() {
|
|
|
15
16
|
await login();
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const token = getToken();
|
|
19
|
-
if (!token) {
|
|
20
|
-
console.error("Authentication failed: no token returned by Poke SDK.");
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
19
|
const dest = join(tmpdir(), `poke-gate-screenshot-${Date.now()}.png`);
|
|
25
20
|
|
|
26
21
|
console.log("Capturing screenshot...");
|
|
@@ -37,8 +32,7 @@ export async function takeScreenshot() {
|
|
|
37
32
|
console.log(`Screenshot captured (${(png.length / 1024).toFixed(0)} KB). Sending to Poke...`);
|
|
38
33
|
|
|
39
34
|
try {
|
|
40
|
-
|
|
41
|
-
await poke.sendMessage(
|
|
35
|
+
await sendToWebhook(
|
|
42
36
|
`Here is a screenshot of my screen right now. Reply me with the image.\n\n\`\`\`\ndata:image/png;base64,${base64}\n\`\`\``
|
|
43
37
|
);
|
|
44
38
|
console.log("Screenshot sent to Poke.");
|
package/src/tunnel.js
CHANGED
|
@@ -1,31 +1,12 @@
|
|
|
1
|
-
import { PokeTunnel
|
|
2
|
-
import {
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
|
|
6
|
-
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
7
|
-
const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
|
|
1
|
+
import { PokeTunnel } from "poke";
|
|
2
|
+
import { loadState, saveState } from "./webhook.js";
|
|
8
3
|
|
|
9
4
|
function log(msg) {
|
|
10
5
|
const ts = new Date().toISOString().slice(11, 19);
|
|
11
6
|
console.log(`[${ts}] ${msg}`);
|
|
12
7
|
}
|
|
13
8
|
|
|
14
|
-
function
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
|
|
17
|
-
} catch {
|
|
18
|
-
return {};
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function saveState(state) {
|
|
23
|
-
mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
|
|
24
|
-
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function cleanupStaleConnections() {
|
|
28
|
-
const token = getToken();
|
|
9
|
+
async function cleanupStaleConnections(token) {
|
|
29
10
|
if (!token) return;
|
|
30
11
|
const base = process.env.POKE_API ?? "https://poke.com/api/v1";
|
|
31
12
|
const state = loadState();
|
|
@@ -49,13 +30,13 @@ async function cleanupStaleConnections() {
|
|
|
49
30
|
} catch {}
|
|
50
31
|
}
|
|
51
32
|
|
|
52
|
-
|
|
33
|
+
const { webhookUrl, webhookToken } = loadState();
|
|
34
|
+
saveState({ webhookUrl, webhookToken });
|
|
53
35
|
}
|
|
54
36
|
|
|
55
|
-
export async function startTunnel({ mcpUrl, onEvent }) {
|
|
56
|
-
await cleanupStaleConnections();
|
|
37
|
+
export async function startTunnel({ mcpUrl, token, onEvent }) {
|
|
38
|
+
await cleanupStaleConnections(token);
|
|
57
39
|
|
|
58
|
-
const token = getToken();
|
|
59
40
|
if (!token) {
|
|
60
41
|
throw new Error("No Poke auth token available for tunnel.");
|
|
61
42
|
}
|
|
@@ -72,6 +53,7 @@ export async function startTunnel({ mcpUrl, onEvent }) {
|
|
|
72
53
|
const history = state.connectionHistory || [];
|
|
73
54
|
history.push(info.connectionId);
|
|
74
55
|
saveState({
|
|
56
|
+
...state,
|
|
75
57
|
connectionId: info.connectionId,
|
|
76
58
|
connectionHistory: history.slice(-10),
|
|
77
59
|
});
|
package/src/webhook.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { Poke } from "poke";
|
|
5
|
+
import { getPokeAuthToken } from "./poke-auth.js";
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
8
|
+
const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
|
|
9
|
+
|
|
10
|
+
export function loadState() {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(readFileSync(STATE_PATH, "utf-8"));
|
|
13
|
+
} catch {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function saveState(state) {
|
|
19
|
+
mkdirSync(join(CONFIG_DIR, "poke-gate"), { recursive: true });
|
|
20
|
+
writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function getWebhook() {
|
|
24
|
+
const state = loadState();
|
|
25
|
+
if (state.webhookUrl && state.webhookToken) {
|
|
26
|
+
return { webhookUrl: state.webhookUrl, webhookToken: state.webhookToken };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const token = getPokeAuthToken();
|
|
30
|
+
if (!token) throw new Error("No Poke auth token available.");
|
|
31
|
+
|
|
32
|
+
const poke = new Poke({ apiKey: token });
|
|
33
|
+
const result = await poke.createWebhook({ condition: "poke-gate", action: "poke-gate" });
|
|
34
|
+
|
|
35
|
+
const webhook = { webhookUrl: result.webhookUrl, webhookToken: result.webhookToken };
|
|
36
|
+
saveState({ ...state, ...webhook });
|
|
37
|
+
return webhook;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function sendToWebhook(message) {
|
|
41
|
+
const { webhookUrl, webhookToken } = await getWebhook();
|
|
42
|
+
const token = getPokeAuthToken();
|
|
43
|
+
if (!token) throw new Error("No Poke auth token available.");
|
|
44
|
+
|
|
45
|
+
const poke = new Poke({ apiKey: token });
|
|
46
|
+
return poke.sendWebhook({ webhookUrl, webhookToken, data: { message } });
|
|
47
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
import { startMcpServer } from "../src/mcp-server.js";
|
|
6
|
+
|
|
7
|
+
function request({ port, method = "GET", path = "/", headers = {}, body }) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const req = http.request({ hostname: "127.0.0.1", port, method, path, headers }, (res) => {
|
|
10
|
+
let data = "";
|
|
11
|
+
res.on("data", (chunk) => {
|
|
12
|
+
data += chunk;
|
|
13
|
+
if (headers.Accept === "text/event-stream") {
|
|
14
|
+
req.destroy();
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
res.on("end", () => resolve({ res, body: data }));
|
|
18
|
+
res.on("close", () => resolve({ res, body: data }));
|
|
19
|
+
});
|
|
20
|
+
req.on("error", (error) => {
|
|
21
|
+
if (headers.Accept === "text/event-stream") return;
|
|
22
|
+
reject(error);
|
|
23
|
+
});
|
|
24
|
+
if (body) req.write(body);
|
|
25
|
+
req.end();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("MCP POST responses include session id header", async () => {
|
|
30
|
+
const { httpServer, port } = await startMcpServer();
|
|
31
|
+
try {
|
|
32
|
+
const { res, body } = await request({
|
|
33
|
+
port,
|
|
34
|
+
method: "POST",
|
|
35
|
+
path: "/mcp",
|
|
36
|
+
headers: {
|
|
37
|
+
"Content-Type": "application/json",
|
|
38
|
+
"Mcp-Session-Id": "session-1",
|
|
39
|
+
},
|
|
40
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
assert.equal(res.statusCode, 200);
|
|
44
|
+
assert.equal(res.headers["mcp-session-id"], "session-1");
|
|
45
|
+
assert.equal(JSON.parse(body).result.tools.length > 0, true);
|
|
46
|
+
} finally {
|
|
47
|
+
httpServer.close();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("MCP GET supports event stream transport", async () => {
|
|
52
|
+
const { httpServer, port } = await startMcpServer();
|
|
53
|
+
try {
|
|
54
|
+
const { res, body } = await request({
|
|
55
|
+
port,
|
|
56
|
+
path: "/mcp",
|
|
57
|
+
headers: {
|
|
58
|
+
Accept: "text/event-stream",
|
|
59
|
+
"Mcp-Session-Id": "session-2",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
assert.equal(res.statusCode, 200);
|
|
64
|
+
assert.equal(res.headers["content-type"], "text/event-stream");
|
|
65
|
+
assert.equal(res.headers["mcp-session-id"], "session-2");
|
|
66
|
+
assert.match(body, /: connected/);
|
|
67
|
+
} finally {
|
|
68
|
+
httpServer.close();
|
|
69
|
+
}
|
|
70
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { resolvePokeToken } from "../src/poke-auth.js";
|
|
5
|
+
|
|
6
|
+
test("POKE_API_KEY takes precedence over login token", () => {
|
|
7
|
+
assert.equal(resolvePokeToken({ env: { POKE_API_KEY: "from-env" }, token: "from-login" }), "from-env");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("login token is used when POKE_API_KEY is not set", () => {
|
|
11
|
+
assert.equal(resolvePokeToken({ env: {}, token: "from-login" }), "from-login");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("missing env and login token resolves to undefined", () => {
|
|
15
|
+
assert.equal(resolvePokeToken({ env: {}, token: undefined }), undefined);
|
|
16
|
+
});
|