poke-gate 0.3.2 → 0.3.5
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/.github/workflows/npm.yml +14 -3
- package/.github/workflows/release.yml +12 -2
- 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 +2 -2
- package/src/app.js +25 -14
- package/src/mcp-server.js +36 -4
- package/src/poke-auth.js +25 -0
- package/src/tunnel.js +4 -6
- package/src/webhook.js +8 -4
- 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
|
|
@@ -5,6 +5,10 @@ on:
|
|
|
5
5
|
tags:
|
|
6
6
|
- 'v*'
|
|
7
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
8
12
|
jobs:
|
|
9
13
|
publish:
|
|
10
14
|
runs-on: ubuntu-latest
|
|
@@ -18,7 +22,14 @@ jobs:
|
|
|
18
22
|
node-version: 22
|
|
19
23
|
registry-url: https://registry.npmjs.org
|
|
20
24
|
|
|
25
|
+
- name: Verify tag matches package version
|
|
26
|
+
run: |
|
|
27
|
+
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
|
28
|
+
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
|
29
|
+
if [[ "${TAG_VERSION}" != "${PACKAGE_VERSION}" ]]; then
|
|
30
|
+
echo "Tag version ${TAG_VERSION} does not match package.json version ${PACKAGE_VERSION}."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
21
34
|
- name: Publish
|
|
22
|
-
run: npm publish --access public
|
|
23
|
-
env:
|
|
24
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
35
|
+
run: npm publish --access public --provenance
|
|
@@ -139,7 +139,10 @@ jobs:
|
|
|
139
139
|
fi
|
|
140
140
|
|
|
141
141
|
VERSION_NUM="${VERSION#v}"
|
|
142
|
-
git clone https://x-access-token:${TAP_REPO_TOKEN_HOMEBREW}@github.com/f/homebrew-tap.git tap
|
|
142
|
+
if ! git clone https://x-access-token:${TAP_REPO_TOKEN_HOMEBREW}@github.com/f/homebrew-tap.git tap; then
|
|
143
|
+
echo "::warning::Skipping Homebrew tap update because cloning f/homebrew-tap failed."
|
|
144
|
+
exit 0
|
|
145
|
+
fi
|
|
143
146
|
mkdir -p tap/Casks
|
|
144
147
|
cat > tap/Casks/poke-gate.rb << EOF
|
|
145
148
|
cask "poke-gate" do
|
|
@@ -170,5 +173,12 @@ jobs:
|
|
|
170
173
|
git config user.name "github-actions[bot]"
|
|
171
174
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
172
175
|
git add .
|
|
176
|
+
if git diff --cached --quiet; then
|
|
177
|
+
echo "Homebrew cask is already up to date."
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
173
180
|
git commit -m "Update poke-gate to ${VERSION}"
|
|
174
|
-
git push
|
|
181
|
+
if ! git push; then
|
|
182
|
+
echo "::warning::Skipping Homebrew tap update because pushing to f/homebrew-tap failed."
|
|
183
|
+
exit 0
|
|
184
|
+
fi
|
|
@@ -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.4;
|
|
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.4;
|
|
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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poke-gate",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "Expose your machine to your Poke AI assistant via MCP tunnel",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"poke-gate": "
|
|
7
|
+
"poke-gate": "bin/poke-gate.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/app.js",
|
package/src/app.js
CHANGED
|
@@ -2,7 +2,7 @@ import { startMcpServer, enableLogging, getPermissionMode } from "./mcp-server.j
|
|
|
2
2
|
import { startTunnel } from "./tunnel.js";
|
|
3
3
|
import { startAgentScheduler, stopAgentScheduler } from "./agents.js";
|
|
4
4
|
import { sendToWebhook } from "./webhook.js";
|
|
5
|
-
import {
|
|
5
|
+
import { ensurePokeAuthenticated } from "./poke-auth.js";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
7
|
|
|
8
8
|
const verbose = process.argv.includes("--verbose") || process.argv.includes("-v");
|
|
@@ -12,8 +12,28 @@ function killExistingInstances() {
|
|
|
12
12
|
const myPid = process.pid;
|
|
13
13
|
const ppid = process.ppid;
|
|
14
14
|
try {
|
|
15
|
-
const out = execSync("
|
|
16
|
-
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);
|
|
17
37
|
for (const pid of pids) {
|
|
18
38
|
try { process.kill(pid, "SIGTERM"); } catch {}
|
|
19
39
|
}
|
|
@@ -31,17 +51,7 @@ function sleep(ms) {
|
|
|
31
51
|
}
|
|
32
52
|
|
|
33
53
|
async function ensureAuthenticated() {
|
|
34
|
-
|
|
35
|
-
log("Signing in to Poke...");
|
|
36
|
-
await login();
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const token = getToken();
|
|
40
|
-
if (!token) {
|
|
41
|
-
throw new Error("Authentication failed: no token returned by Poke SDK.");
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return token;
|
|
54
|
+
return ensurePokeAuthenticated({ onLogin: () => log("Signing in to Poke...") });
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
let currentTunnel = null;
|
|
@@ -60,6 +70,7 @@ async function connectWithRetry(mcpUrl, token) {
|
|
|
60
70
|
|
|
61
71
|
const { tunnel } = await startTunnel({
|
|
62
72
|
mcpUrl,
|
|
73
|
+
token,
|
|
63
74
|
onEvent: (type, data) => {
|
|
64
75
|
switch (type) {
|
|
65
76
|
case "connected":
|
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/tunnel.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PokeTunnel
|
|
1
|
+
import { PokeTunnel } from "poke";
|
|
2
2
|
import { loadState, saveState } from "./webhook.js";
|
|
3
3
|
|
|
4
4
|
function log(msg) {
|
|
@@ -6,8 +6,7 @@ function log(msg) {
|
|
|
6
6
|
console.log(`[${ts}] ${msg}`);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async function cleanupStaleConnections() {
|
|
10
|
-
const token = getToken();
|
|
9
|
+
async function cleanupStaleConnections(token) {
|
|
11
10
|
if (!token) return;
|
|
12
11
|
const base = process.env.POKE_API ?? "https://poke.com/api/v1";
|
|
13
12
|
const state = loadState();
|
|
@@ -35,10 +34,9 @@ async function cleanupStaleConnections() {
|
|
|
35
34
|
saveState({ webhookUrl, webhookToken });
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
export async function startTunnel({ mcpUrl, onEvent }) {
|
|
39
|
-
await cleanupStaleConnections();
|
|
37
|
+
export async function startTunnel({ mcpUrl, token, onEvent }) {
|
|
38
|
+
await cleanupStaleConnections(token);
|
|
40
39
|
|
|
41
|
-
const token = getToken();
|
|
42
40
|
if (!token) {
|
|
43
41
|
throw new Error("No Poke auth token available for tunnel.");
|
|
44
42
|
}
|
package/src/webhook.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { Poke
|
|
4
|
+
import { Poke } from "poke";
|
|
5
|
+
import { getPokeAuthToken } from "./poke-auth.js";
|
|
5
6
|
|
|
6
7
|
const CONFIG_DIR = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
7
8
|
const STATE_PATH = join(CONFIG_DIR, "poke-gate", "state.json");
|
|
@@ -25,10 +26,10 @@ export async function getWebhook() {
|
|
|
25
26
|
return { webhookUrl: state.webhookUrl, webhookToken: state.webhookToken };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const token =
|
|
29
|
+
const token = getPokeAuthToken();
|
|
29
30
|
if (!token) throw new Error("No Poke auth token available.");
|
|
30
31
|
|
|
31
|
-
const poke = new Poke({ token });
|
|
32
|
+
const poke = new Poke({ apiKey: token });
|
|
32
33
|
const result = await poke.createWebhook({ condition: "poke-gate", action: "poke-gate" });
|
|
33
34
|
|
|
34
35
|
const webhook = { webhookUrl: result.webhookUrl, webhookToken: result.webhookToken };
|
|
@@ -38,6 +39,9 @@ export async function getWebhook() {
|
|
|
38
39
|
|
|
39
40
|
export async function sendToWebhook(message) {
|
|
40
41
|
const { webhookUrl, webhookToken } = await getWebhook();
|
|
41
|
-
const
|
|
42
|
+
const token = getPokeAuthToken();
|
|
43
|
+
if (!token) throw new Error("No Poke auth token available.");
|
|
44
|
+
|
|
45
|
+
const poke = new Poke({ apiKey: token });
|
|
42
46
|
return poke.sendWebhook({ webhookUrl, webhookToken, data: { message } });
|
|
43
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
|
+
});
|