portveil-mcp 0.4.2 → 0.4.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/README.md +1 -1
- package/dist/index.js +7 -4
- package/dist/portveil.js +38 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ claude mcp add portveil -e PORTVEIL_ACCOUNT_ID=acct_… -e PORTVEIL_TOKEN=clt_
|
|
|
60
60
|
|
|
61
61
|
**Hermes Agent**: Hermes only installs npm packages older than 14 days, so install into its own folder and run it with node:
|
|
62
62
|
```bash
|
|
63
|
-
cd ~ && npm install --prefix ~/.hermes/mcp-servers/portveil portveil-mcp@
|
|
63
|
+
cd ~ && npm install --prefix ~/.hermes/mcp-servers/portveil portveil-mcp@0.4.3
|
|
64
64
|
echo 'PORTVEIL_TOKEN=clt_…' >> ~/.hermes/.env
|
|
65
65
|
hermes mcp add portveil --command node \
|
|
66
66
|
--env PORTVEIL_ACCOUNT_ID=acct_… 'PORTVEIL_TOKEN=${PORTVEIL_TOKEN}' \
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { join, resolve } from "node:path";
|
|
16
|
-
import { Portveil, PortveilError, VERSION, agentSetup, describeDevice, wgConfig, wgKeypair, isWaiting, locationLabel, nextLocation, resolveDevice, resolveLocation, } from "./portveil.js";
|
|
16
|
+
import { Portveil, PortveilError, VERSION, agentSetup, describeDevice, wgConfig, wgKeypair, isWaiting, localHint, locationLabel, nextLocation, resolveDevice, resolveLocation, } from "./portveil.js";
|
|
17
17
|
const accountId = process.env.PORTVEIL_ACCOUNT_ID?.trim() ?? "";
|
|
18
18
|
const token = process.env.PORTVEIL_TOKEN?.trim() ?? "";
|
|
19
19
|
if (!/^acct_[0-9a-f]{16}$/.test(accountId) || !token) {
|
|
@@ -52,12 +52,15 @@ server.registerTool("list_devices", {
|
|
|
52
52
|
}));
|
|
53
53
|
server.registerTool("list_locations", {
|
|
54
54
|
title: "List locations",
|
|
55
|
-
description: "List the exit locations (country, city and location ID) that devices can be moved to. Use it before move_device or start_rotation when you're unsure what's available. Every plan can use every location. Read-only.",
|
|
55
|
+
description: "List the exit locations (country, city and location ID) that devices can be moved to, with each one's timezone and language settings. Use it before move_device or start_rotation when you're unsure what's available, or to make a browser on a moved machine match its exit (sites compare the browser's timezone with its IP's country). Every plan can use every location. Read-only.",
|
|
56
56
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
57
|
-
}, () => run(async () => (await pv.servers()).map((s) =>
|
|
57
|
+
}, () => run(async () => (await pv.servers()).map((s) => {
|
|
58
|
+
const hint = localHint(s);
|
|
59
|
+
return `- ${locationLabel(s)} [${s.id}]${hint ? `: ${hint}` : ""}`;
|
|
60
|
+
}).join("\n")));
|
|
58
61
|
server.registerTool("get_device", {
|
|
59
62
|
title: "Get device",
|
|
60
|
-
description: "Get one device's current state: online or offline, the location it exits from, whether that exit server confirms the tunnel, its live speed, any rotation schedule, and when it last reported. Use it to check a device before or after an action. Read-only.",
|
|
63
|
+
description: "Get one device's current state: online or offline, the location it exits from, whether that exit server confirms the tunnel, its live speed, any rotation schedule, whether an agent machine's clock matches its exit's timezone, and when it last reported. Use it to check a device before or after an action. Read-only.",
|
|
61
64
|
inputSchema: { device: deviceArg },
|
|
62
65
|
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
63
66
|
}, ({ device }) => run(async () => {
|
package/dist/portveil.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Portveil API client and the logic behind the MCP tools. No MCP types here,
|
|
2
2
|
// so it can be tested against a fake API.
|
|
3
3
|
import { generateKeyPairSync } from "node:crypto";
|
|
4
|
-
export const VERSION = "0.4.
|
|
4
|
+
export const VERSION = "0.4.3";
|
|
5
5
|
/** "↓ 42.3 Mbps ↑ 6.1 Mbps", or "idle" when nothing is moving. */
|
|
6
6
|
export function describeSpeed(speed) {
|
|
7
7
|
if (speed.down_mbps < 0.1 && speed.up_mbps < 0.1)
|
|
@@ -39,9 +39,40 @@ export class PortveilError extends Error {
|
|
|
39
39
|
export const isWaiting = (status) => status === "queued" || status === "delivered" || status === "pending";
|
|
40
40
|
// Friendly names for the exits we run; anything new falls back to the server's own name.
|
|
41
41
|
const KNOWN = {
|
|
42
|
-
"srv-us-1": { country: "United States", code: "US", place: "US West", aliases: ["usa", "america", "united states", "us", "us west", "west"]
|
|
43
|
-
|
|
42
|
+
"srv-us-1": { country: "United States", code: "US", place: "US West", aliases: ["usa", "america", "united states", "us", "us west", "west"],
|
|
43
|
+
timezone: "America/Los_Angeles", locale: "en-US", accept_language: "en-US,en;q=0.9" },
|
|
44
|
+
"srv-eu-1": { country: "Finland", code: "FI", place: "Helsinki", aliases: ["finland", "fi", "helsinki", "eu", "europe"],
|
|
45
|
+
timezone: "Europe/Helsinki", locale: "fi-FI", accept_language: "en-US,en;q=0.9,fi;q=0.8" },
|
|
44
46
|
};
|
|
47
|
+
/** What a local browser at this exit looks like: the API's values, else the built-in ones. */
|
|
48
|
+
export function localProfile(s) {
|
|
49
|
+
const k = KNOWN[s.id];
|
|
50
|
+
const timezone = s.timezone ?? k?.timezone;
|
|
51
|
+
if (!timezone)
|
|
52
|
+
return undefined;
|
|
53
|
+
return { timezone, locale: s.locale ?? k?.locale ?? "", accept_language: s.accept_language ?? k?.accept_language ?? "" };
|
|
54
|
+
}
|
|
55
|
+
/** One line an agent can act on so its browser matches the exit. Match the timezone;
|
|
56
|
+
* keep the language English-first so pages stay readable. */
|
|
57
|
+
export function localHint(s) {
|
|
58
|
+
const p = localProfile(s);
|
|
59
|
+
if (!p)
|
|
60
|
+
return "";
|
|
61
|
+
const lang = p.accept_language ? `; keep the browser's language English-first (Accept-Language: ${p.accept_language})` : "";
|
|
62
|
+
const local = p.locale && !p.locale.startsWith("en") ? `; the local language is ${p.locale}, only for pages that should be in it` : "";
|
|
63
|
+
return `timezone ${p.timezone}${lang}${local}`;
|
|
64
|
+
}
|
|
65
|
+
/** Whether the machine's own clock matches its exit (agent machines only). */
|
|
66
|
+
export function clockNote(d, where) {
|
|
67
|
+
if (!d.connected || !d.timezone || !where)
|
|
68
|
+
return "";
|
|
69
|
+
const want = localProfile(where)?.timezone;
|
|
70
|
+
if (!want)
|
|
71
|
+
return "";
|
|
72
|
+
return d.timezone === want
|
|
73
|
+
? `clock ${d.timezone}, matching the exit`
|
|
74
|
+
: `clock set to ${d.timezone}, not the exit's ${want}: sites can see the mismatch, so set a browser on it to ${want}`;
|
|
75
|
+
}
|
|
45
76
|
export function locationLabel(s) {
|
|
46
77
|
const k = KNOWN[s.id];
|
|
47
78
|
return k ? `${k.country} (${k.place})` : s.name;
|
|
@@ -64,6 +95,9 @@ export function describeDevice(d, servers) {
|
|
|
64
95
|
extras.push(`speed ${describeSpeed(d.speed)}`);
|
|
65
96
|
if (d.rotation)
|
|
66
97
|
extras.push(`rotates every ${d.rotation.every_minutes} min`);
|
|
98
|
+
const clock = clockNote(d, where);
|
|
99
|
+
if (clock)
|
|
100
|
+
extras.push(clock);
|
|
67
101
|
if (d.expires_at)
|
|
68
102
|
extras.push(`temporary, deleted ${new Date(d.expires_at * 1000).toISOString().replace(".000Z", "Z")}`);
|
|
69
103
|
return `${d.name} [${d.device_id}] (${d.platform}): ${state}; ${remote}${extras.length ? `; ${extras.join("; ")}` : ""}`;
|
|
@@ -240,7 +274,7 @@ export class Portveil {
|
|
|
240
274
|
return `${device.name} did not move: ${st.status}${st.result ? ` (${st.result})` : ""}. It is still on its previous location.`;
|
|
241
275
|
const confirmed = await this.waitConfirmed(device.device_id, target.id);
|
|
242
276
|
return confirmed
|
|
243
|
-
? `Done. ${device.name} now exits in ${place}, confirmed by the ${place} exit server.`
|
|
277
|
+
? `Done. ${device.name} now exits in ${place}, confirmed by the ${place} exit server.${localHint(target) ? ` If a browser on it should look local, use ${localHint(target)}.` : ""}`
|
|
244
278
|
: `${device.name} switched to ${place}, but that exit hasn't confirmed the connection yet. Check again with get_device in a minute.`;
|
|
245
279
|
}
|
|
246
280
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "portveil-mcp",
|
|
3
3
|
"mcpName": "io.github.roybogs/portveil-mcp",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"description": "MCP server for Portveil: let AI assistants see your devices and move them between VPN locations.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|