virlow-mcp 3.17.1 → 3.19.0
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 +28 -7
- package/dist/cli.js +205 -110
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -35,13 +35,13 @@ offers a **hosted connector** — the same 19 tools, served from Virlow's own se
|
|
|
35
35
|
client can connect by URL with nothing installed. It is a genuinely different trust model,
|
|
36
36
|
and the sentence "the key never leaves it" is the part that stops being true:
|
|
37
37
|
|
|
38
|
-
| | this local server | the hosted connector |
|
|
39
|
-
| --- | --- | --- |
|
|
40
|
-
| where the key is derived | this process, on your machine | Virlow's API process |
|
|
41
|
-
| who can read a decrypted note | this process | this process, and Virlow's server |
|
|
42
|
-
| what an unlock sends | nothing; the password stays local | your master password, over HTTPS |
|
|
43
|
-
| what it takes to run | Node on your machine | nothing |
|
|
44
|
-
| session lifetime | 4 hours idle | 30 minutes idle |
|
|
38
|
+
| | this local server | the desktop app | the hosted connector |
|
|
39
|
+
| --- | --- | --- | --- |
|
|
40
|
+
| where the key is derived | this process, on your machine | the Virlow app, on your machine | Virlow's API process |
|
|
41
|
+
| who can read a decrypted note | this process | the app's process | this process, and Virlow's server |
|
|
42
|
+
| what an unlock sends | nothing; the password stays local | nothing; the password stays local | your master password, over HTTPS |
|
|
43
|
+
| what it takes to run | Node on your machine | the installed app | nothing |
|
|
44
|
+
| session lifetime | 4 hours idle | follows the app's vault lock | 30 minutes idle |
|
|
45
45
|
|
|
46
46
|
The hosted connector is **off by default** on every account and has to be switched on in
|
|
47
47
|
Virlow's settings. If you are reading this page, you are looking at the option that keeps
|
|
@@ -122,6 +122,27 @@ Claude Desktop / Cursor:
|
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### Connecting through the Virlow desktop app
|
|
126
|
+
|
|
127
|
+
If you run the [Virlow desktop app](../desktop/README.md), it already hosts this server
|
|
128
|
+
on your machine and unlocks with the app. Clients that can send a header connect to it
|
|
129
|
+
directly; the app shows the exact snippet under Settings, This computer. Clients that can
|
|
130
|
+
only spawn a command use this package as a pipe:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"virlow": {
|
|
136
|
+
"command": "npx",
|
|
137
|
+
"args": ["-y", "virlow-mcp", "--connect", "http://127.0.0.1:47821/mcp",
|
|
138
|
+
"--token-file", "<path the app shows you>"]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
In this mode nothing is decrypted in this process. It forwards to the app and back.
|
|
145
|
+
|
|
125
146
|
## First use
|
|
126
147
|
|
|
127
148
|
1. Ask your AI client to run the **`unlock`** tool. This opens a browser window pointed
|
package/dist/cli.js
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import * as os2 from "node:os";
|
|
5
5
|
import * as path4 from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
7
|
+
import { StdioServerTransport as StdioServerTransport2 } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
8
|
|
|
8
9
|
// src/server.ts
|
|
9
10
|
import os from "node:os";
|
|
10
|
-
import
|
|
11
|
+
import path3 from "node:path";
|
|
11
12
|
|
|
12
13
|
// ../../packages/mcp-core/dist/api.js
|
|
13
14
|
var ApiError = class extends Error {
|
|
@@ -50,12 +51,29 @@ var VirlowApi = class {
|
|
|
50
51
|
tokens = null;
|
|
51
52
|
refreshInFlight = null;
|
|
52
53
|
onTokensRotated;
|
|
54
|
+
/** Fired when a refresh is refused outright and the session is gone. */
|
|
55
|
+
onTokensCleared;
|
|
53
56
|
constructor(baseUrl) {
|
|
54
57
|
this.baseUrl = baseUrl;
|
|
55
58
|
}
|
|
56
59
|
setTokens(tokens) {
|
|
57
60
|
this.tokens = tokens;
|
|
58
61
|
}
|
|
62
|
+
getTokens() {
|
|
63
|
+
return this.tokens;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Rotate the refresh token exactly once no matter how many callers arrive
|
|
67
|
+
* while a rotation is pending. The API revokes the whole token family when a
|
|
68
|
+
* replaced refresh token is presented again, so two concurrent refreshes are
|
|
69
|
+
* not a race, they are a forced sign-out.
|
|
70
|
+
*/
|
|
71
|
+
refreshSession() {
|
|
72
|
+
this.refreshInFlight ??= this.doRefresh().finally(() => {
|
|
73
|
+
this.refreshInFlight = null;
|
|
74
|
+
});
|
|
75
|
+
return this.refreshInFlight;
|
|
76
|
+
}
|
|
59
77
|
async request(method, path5, body, opts = {}) {
|
|
60
78
|
const { auth = true, retryOn401 = true } = opts;
|
|
61
79
|
const headers = { "content-type": "application/json" };
|
|
@@ -79,17 +97,24 @@ var VirlowApi = class {
|
|
|
79
97
|
return data;
|
|
80
98
|
}
|
|
81
99
|
refresh() {
|
|
82
|
-
|
|
83
|
-
this.refreshInFlight = null;
|
|
84
|
-
});
|
|
85
|
-
return this.refreshInFlight;
|
|
100
|
+
return this.refreshSession().then(() => void 0);
|
|
86
101
|
}
|
|
87
102
|
async doRefresh() {
|
|
88
103
|
if (!this.tokens)
|
|
89
104
|
throw new ApiError(401, "Not authenticated");
|
|
90
|
-
|
|
105
|
+
let rotated;
|
|
106
|
+
try {
|
|
107
|
+
rotated = await this.request("POST", "/api/auth/refresh", { refreshToken: this.tokens.refreshToken }, { auth: false, retryOn401: false });
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof ApiError && err.status === 401) {
|
|
110
|
+
this.tokens = null;
|
|
111
|
+
this.onTokensCleared?.();
|
|
112
|
+
}
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
91
115
|
this.tokens = { accessToken: rotated.accessToken, refreshToken: rotated.refreshToken };
|
|
92
116
|
this.onTokensRotated?.(this.tokens);
|
|
117
|
+
return { user: rotated.user, accessToken: rotated.accessToken };
|
|
93
118
|
}
|
|
94
119
|
login(email, password) {
|
|
95
120
|
return this.request("POST", "/api/auth/login", { email, password }, { auth: false });
|
|
@@ -1411,7 +1436,7 @@ function formatMeta(meta) {
|
|
|
1411
1436
|
}
|
|
1412
1437
|
var truncate = (text, max = 120) => text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
1413
1438
|
function createServices(opts) {
|
|
1414
|
-
const api = new VirlowApi(opts.apiUrl);
|
|
1439
|
+
const api = opts.api ?? new VirlowApi(opts.apiUrl);
|
|
1415
1440
|
const vault = new Vault(opts.autoLockMs);
|
|
1416
1441
|
let embedderPromise = null;
|
|
1417
1442
|
const getRealEmbedder = () => {
|
|
@@ -1684,21 +1709,30 @@ ${note.content}`);
|
|
|
1684
1709
|
}
|
|
1685
1710
|
|
|
1686
1711
|
// src/unlock.ts
|
|
1687
|
-
import { randomBytes } from "node:crypto";
|
|
1712
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1688
1713
|
import { spawn } from "node:child_process";
|
|
1689
1714
|
import * as http from "node:http";
|
|
1690
1715
|
|
|
1691
|
-
//
|
|
1716
|
+
// ../../packages/mcp-core/dist/http/server.js
|
|
1717
|
+
import { createServer } from "node:http";
|
|
1718
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1719
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
1720
|
+
import path2 from "node:path";
|
|
1721
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1722
|
+
|
|
1723
|
+
// ../../packages/mcp-core/dist/http/local-origin.js
|
|
1692
1724
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]", "::1"]);
|
|
1693
1725
|
function withoutPort(host) {
|
|
1694
1726
|
return host.replace(/:\d+$/, "").toLowerCase();
|
|
1695
1727
|
}
|
|
1696
1728
|
function isLoopbackHost(hostHeader) {
|
|
1697
|
-
if (hostHeader === void 0 || hostHeader === "")
|
|
1729
|
+
if (hostHeader === void 0 || hostHeader === "")
|
|
1730
|
+
return false;
|
|
1698
1731
|
return LOOPBACK_HOSTS.has(withoutPort(hostHeader.trim()));
|
|
1699
1732
|
}
|
|
1700
1733
|
function isAllowedOrigin(originHeader) {
|
|
1701
|
-
if (originHeader === void 0 || originHeader === "")
|
|
1734
|
+
if (originHeader === void 0 || originHeader === "")
|
|
1735
|
+
return true;
|
|
1702
1736
|
try {
|
|
1703
1737
|
return LOOPBACK_HOSTS.has(new URL(originHeader).hostname.toLowerCase());
|
|
1704
1738
|
} catch {
|
|
@@ -1706,9 +1740,12 @@ function isAllowedOrigin(originHeader) {
|
|
|
1706
1740
|
}
|
|
1707
1741
|
}
|
|
1708
1742
|
function rejectReason(headers, enforce) {
|
|
1709
|
-
if (!enforce)
|
|
1710
|
-
|
|
1711
|
-
if (!
|
|
1743
|
+
if (!enforce)
|
|
1744
|
+
return null;
|
|
1745
|
+
if (!isLoopbackHost(headers.host))
|
|
1746
|
+
return "non-loopback Host";
|
|
1747
|
+
if (!isAllowedOrigin(headers.origin))
|
|
1748
|
+
return "cross-site Origin";
|
|
1712
1749
|
return null;
|
|
1713
1750
|
}
|
|
1714
1751
|
var SECURITY_HEADERS = Object.freeze({
|
|
@@ -1719,6 +1756,99 @@ var SECURITY_HEADERS = Object.freeze({
|
|
|
1719
1756
|
"cache-control": "no-store"
|
|
1720
1757
|
});
|
|
1721
1758
|
|
|
1759
|
+
// ../../packages/mcp-core/dist/http/server.js
|
|
1760
|
+
function defaultTokenPath(homeDir) {
|
|
1761
|
+
return path2.join(homeDir, ".virlow-mcp", "http-token");
|
|
1762
|
+
}
|
|
1763
|
+
async function loadOrCreateToken(tokenPath) {
|
|
1764
|
+
try {
|
|
1765
|
+
const existing = (await readFile2(tokenPath, "utf8")).trim();
|
|
1766
|
+
if (existing.length > 0)
|
|
1767
|
+
return existing;
|
|
1768
|
+
} catch {
|
|
1769
|
+
}
|
|
1770
|
+
const token = randomBytes(32).toString("base64url");
|
|
1771
|
+
await mkdir2(path2.dirname(tokenPath), { recursive: true, mode: 448 });
|
|
1772
|
+
await writeFile2(tokenPath, `${token}
|
|
1773
|
+
`, { mode: 384 });
|
|
1774
|
+
return token;
|
|
1775
|
+
}
|
|
1776
|
+
function tokenMatches(provided, expected) {
|
|
1777
|
+
const a = Buffer.from(provided);
|
|
1778
|
+
const b = Buffer.from(expected);
|
|
1779
|
+
if (a.length !== b.length) {
|
|
1780
|
+
timingSafeEqual(b, b);
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
return timingSafeEqual(a, b);
|
|
1784
|
+
}
|
|
1785
|
+
function bearerFrom(header) {
|
|
1786
|
+
if (!header)
|
|
1787
|
+
return null;
|
|
1788
|
+
const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());
|
|
1789
|
+
return match ? match[1].trim() : null;
|
|
1790
|
+
}
|
|
1791
|
+
async function handleWithFreshTransport(server, req, res) {
|
|
1792
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1793
|
+
sessionIdGenerator: void 0
|
|
1794
|
+
});
|
|
1795
|
+
res.on("close", () => {
|
|
1796
|
+
void transport.close();
|
|
1797
|
+
void server.close();
|
|
1798
|
+
});
|
|
1799
|
+
await server.connect(transport);
|
|
1800
|
+
await transport.handleRequest(req, res);
|
|
1801
|
+
}
|
|
1802
|
+
async function startHttpServer(opts) {
|
|
1803
|
+
const host = opts.host ?? "127.0.0.1";
|
|
1804
|
+
const mcpPath = opts.path ?? "/mcp";
|
|
1805
|
+
const enforceLocal = host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
1806
|
+
const httpServer = createServer((req, res) => {
|
|
1807
|
+
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
1808
|
+
if (rejectReason(req.headers, enforceLocal) !== null) {
|
|
1809
|
+
res.writeHead(403, { ...SECURITY_HEADERS, "content-type": "application/json" });
|
|
1810
|
+
res.end(JSON.stringify({ error: "forbidden" }));
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
if (url.pathname === "/health") {
|
|
1814
|
+
res.writeHead(200, { ...SECURITY_HEADERS, "content-type": "application/json" });
|
|
1815
|
+
res.end(JSON.stringify({ ok: true }));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
if (url.pathname !== mcpPath) {
|
|
1819
|
+
res.writeHead(404, SECURITY_HEADERS).end();
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const provided = bearerFrom(req.headers.authorization);
|
|
1823
|
+
if (provided === null || opts.authorize(provided) === null) {
|
|
1824
|
+
res.writeHead(401, {
|
|
1825
|
+
...SECURITY_HEADERS,
|
|
1826
|
+
"content-type": "application/json",
|
|
1827
|
+
"www-authenticate": "Bearer"
|
|
1828
|
+
});
|
|
1829
|
+
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
void handleWithFreshTransport(opts.makeServer(), req, res);
|
|
1833
|
+
});
|
|
1834
|
+
await new Promise((resolve, reject) => {
|
|
1835
|
+
httpServer.once("error", reject);
|
|
1836
|
+
httpServer.listen(opts.port ?? 0, host, () => {
|
|
1837
|
+
httpServer.removeListener("error", reject);
|
|
1838
|
+
resolve();
|
|
1839
|
+
});
|
|
1840
|
+
});
|
|
1841
|
+
const address = httpServer.address();
|
|
1842
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
1843
|
+
return {
|
|
1844
|
+
port,
|
|
1845
|
+
host,
|
|
1846
|
+
close: () => new Promise((resolve) => {
|
|
1847
|
+
httpServer.close(() => resolve());
|
|
1848
|
+
})
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1722
1852
|
// src/unlock.ts
|
|
1723
1853
|
var UNLOCK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
1724
1854
|
var MAX_BAD_NONCE_ATTEMPTS = 3;
|
|
@@ -1859,7 +1989,7 @@ function openInBrowser(url) {
|
|
|
1859
1989
|
function startUnlockServer(api, vault, opts = {}) {
|
|
1860
1990
|
const openBrowser = opts.openBrowser ?? true;
|
|
1861
1991
|
return new Promise((resolveStart, rejectStart) => {
|
|
1862
|
-
const nonce =
|
|
1992
|
+
const nonce = randomBytes2(32).toString("hex");
|
|
1863
1993
|
let badNonceAttempts = 0;
|
|
1864
1994
|
let settled = false;
|
|
1865
1995
|
let timeoutHandle;
|
|
@@ -1984,11 +2114,11 @@ function startUnlockServer(api, vault, opts = {}) {
|
|
|
1984
2114
|
}
|
|
1985
2115
|
|
|
1986
2116
|
// src/version.ts
|
|
1987
|
-
var VERSION = true ? "3.
|
|
2117
|
+
var VERSION = true ? "3.19.0" : "dev";
|
|
1988
2118
|
|
|
1989
2119
|
// src/server.ts
|
|
1990
2120
|
function defaultEmbeddingCacheDir() {
|
|
1991
|
-
return
|
|
2121
|
+
return path3.join(os.homedir(), ".virlow-mcp", "cache");
|
|
1992
2122
|
}
|
|
1993
2123
|
var localBrowserUnlock = {
|
|
1994
2124
|
description: "Open a local browser window where the USER signs in and enters their Virlow master password. This tool returns immediately once the browser is opened \u2014 it does NOT wait for the user to finish. Call the status tool afterward to confirm the vault unlocked. Never ask the user for their password or account credentials yourself, and never relay a password through this tool or any other \u2014 credentials are entered only in the browser form and never seen by any AI model.",
|
|
@@ -2017,99 +2147,53 @@ function buildServer2(opts) {
|
|
|
2017
2147
|
return buildServer({ ...withLocalDefaults(opts), services: opts.services });
|
|
2018
2148
|
}
|
|
2019
2149
|
|
|
2020
|
-
// src/
|
|
2021
|
-
import {
|
|
2022
|
-
import {
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
} catch {
|
|
2034
|
-
}
|
|
2035
|
-
const token = randomBytes2(32).toString("base64url");
|
|
2036
|
-
await mkdir2(path3.dirname(tokenPath), { recursive: true, mode: 448 });
|
|
2037
|
-
await writeFile2(tokenPath, `${token}
|
|
2038
|
-
`, { mode: 384 });
|
|
2039
|
-
return token;
|
|
2040
|
-
}
|
|
2041
|
-
function tokenMatches(provided, expected) {
|
|
2042
|
-
const a = Buffer.from(provided);
|
|
2043
|
-
const b = Buffer.from(expected);
|
|
2044
|
-
if (a.length !== b.length) {
|
|
2045
|
-
timingSafeEqual(b, b);
|
|
2046
|
-
return false;
|
|
2047
|
-
}
|
|
2048
|
-
return timingSafeEqual(a, b);
|
|
2049
|
-
}
|
|
2050
|
-
function bearerFrom(header) {
|
|
2051
|
-
if (!header) return null;
|
|
2052
|
-
const match = /^Bearer[ ]+(.+)$/i.exec(header.trim());
|
|
2053
|
-
return match ? match[1].trim() : null;
|
|
2054
|
-
}
|
|
2055
|
-
async function handleWithFreshTransport(server, req, res) {
|
|
2056
|
-
const transport = new StreamableHTTPServerTransport({
|
|
2057
|
-
sessionIdGenerator: void 0
|
|
2058
|
-
});
|
|
2059
|
-
res.on("close", () => {
|
|
2060
|
-
void transport.close();
|
|
2061
|
-
void server.close();
|
|
2062
|
-
});
|
|
2063
|
-
await server.connect(transport);
|
|
2064
|
-
await transport.handleRequest(req, res);
|
|
2065
|
-
}
|
|
2066
|
-
async function startHttpServer(opts) {
|
|
2067
|
-
const host = opts.host ?? "127.0.0.1";
|
|
2068
|
-
const mcpPath = opts.path ?? "/mcp";
|
|
2069
|
-
const enforceLocal = host === "127.0.0.1" || host === "::1" || host === "localhost";
|
|
2070
|
-
const httpServer = createServer2((req, res) => {
|
|
2071
|
-
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
2072
|
-
if (rejectReason(req.headers, enforceLocal) !== null) {
|
|
2073
|
-
res.writeHead(403, { ...SECURITY_HEADERS, "content-type": "application/json" });
|
|
2074
|
-
res.end(JSON.stringify({ error: "forbidden" }));
|
|
2075
|
-
return;
|
|
2076
|
-
}
|
|
2077
|
-
if (url.pathname === "/health") {
|
|
2078
|
-
res.writeHead(200, { ...SECURITY_HEADERS, "content-type": "application/json" });
|
|
2079
|
-
res.end(JSON.stringify({ ok: true }));
|
|
2080
|
-
return;
|
|
2081
|
-
}
|
|
2082
|
-
if (url.pathname !== mcpPath) {
|
|
2083
|
-
res.writeHead(404, SECURITY_HEADERS).end();
|
|
2084
|
-
return;
|
|
2085
|
-
}
|
|
2086
|
-
const provided = bearerFrom(req.headers.authorization);
|
|
2087
|
-
if (provided === null || !tokenMatches(provided, opts.token)) {
|
|
2088
|
-
res.writeHead(401, {
|
|
2089
|
-
...SECURITY_HEADERS,
|
|
2090
|
-
"content-type": "application/json",
|
|
2091
|
-
"www-authenticate": "Bearer"
|
|
2092
|
-
});
|
|
2093
|
-
res.end(JSON.stringify({ error: "unauthorized" }));
|
|
2094
|
-
return;
|
|
2095
|
-
}
|
|
2096
|
-
void handleWithFreshTransport(opts.makeServer(), req, res);
|
|
2150
|
+
// src/connect.ts
|
|
2151
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2152
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
2153
|
+
async function runConnectBridge(opts) {
|
|
2154
|
+
const report = opts.onError ?? ((m) => process.stderr.write(`virlow-mcp: ${m}
|
|
2155
|
+
`));
|
|
2156
|
+
let closing = false;
|
|
2157
|
+
const local = new StdioServerTransport(
|
|
2158
|
+
opts.stdin,
|
|
2159
|
+
opts.stdout
|
|
2160
|
+
);
|
|
2161
|
+
const remote = new StreamableHTTPClientTransport(new URL(opts.url), {
|
|
2162
|
+
requestInit: { headers: { authorization: `Bearer ${opts.token}` } }
|
|
2097
2163
|
});
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2164
|
+
local.onmessage = (message) => {
|
|
2165
|
+
remote.send(message).catch((err) => {
|
|
2166
|
+
const text = err instanceof Error ? err.message : String(err);
|
|
2167
|
+
if (!closing) report(`could not reach the desktop app at ${opts.url}: ${text}`);
|
|
2168
|
+
if ("id" in message && message.id !== void 0 && "method" in message) {
|
|
2169
|
+
void local.send({
|
|
2170
|
+
jsonrpc: "2.0",
|
|
2171
|
+
id: message.id,
|
|
2172
|
+
error: { code: -32e3, message: `Virlow desktop app unreachable: ${text}` }
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2103
2175
|
});
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2176
|
+
};
|
|
2177
|
+
remote.onmessage = (message) => {
|
|
2178
|
+
local.send(message).catch((err) => {
|
|
2179
|
+
report(`stdio write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2180
|
+
});
|
|
2181
|
+
};
|
|
2182
|
+
local.onclose = () => {
|
|
2183
|
+
closing = true;
|
|
2184
|
+
void remote.close();
|
|
2185
|
+
};
|
|
2186
|
+
remote.onerror = (err) => {
|
|
2187
|
+
if (!closing) report(err.message);
|
|
2188
|
+
};
|
|
2189
|
+
await remote.start();
|
|
2190
|
+
await local.start();
|
|
2107
2191
|
return {
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
}
|
|
2192
|
+
close: async () => {
|
|
2193
|
+
closing = true;
|
|
2194
|
+
await remote.close();
|
|
2195
|
+
await local.close();
|
|
2196
|
+
}
|
|
2113
2197
|
};
|
|
2114
2198
|
}
|
|
2115
2199
|
|
|
@@ -2127,6 +2211,17 @@ async function main() {
|
|
|
2127
2211
|
`);
|
|
2128
2212
|
return;
|
|
2129
2213
|
}
|
|
2214
|
+
const connectUrl = flagValue("connect");
|
|
2215
|
+
if (connectUrl !== void 0) {
|
|
2216
|
+
const tokenFile = flagValue("token-file");
|
|
2217
|
+
if (!tokenFile) {
|
|
2218
|
+
throw new Error("--connect needs --token-file <path> (the Virlow app writes this file for you)");
|
|
2219
|
+
}
|
|
2220
|
+
const token = (await readFile3(tokenFile, "utf8")).trim();
|
|
2221
|
+
if (!token) throw new Error(`token file is empty: ${tokenFile}`);
|
|
2222
|
+
await runConnectBridge({ url: connectUrl, token });
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2130
2225
|
const apiUrl = process.env.VIRLOW_API_URL ?? "https://api.virlow.com";
|
|
2131
2226
|
const modelCacheDir = path4.join(os2.homedir(), ".virlow-mcp", "models");
|
|
2132
2227
|
const embeddingCacheDir = path4.join(os2.homedir(), ".virlow-mcp", "cache");
|
|
@@ -2142,7 +2237,7 @@ async function main() {
|
|
|
2142
2237
|
const host = flagValue("host") ?? "127.0.0.1";
|
|
2143
2238
|
const running = await startHttpServer({
|
|
2144
2239
|
makeServer: () => buildServer2({ ...serverOpts, services }).server,
|
|
2145
|
-
token,
|
|
2240
|
+
authorize: (bearer) => tokenMatches(bearer, token) ? { clientId: "cli" } : null,
|
|
2146
2241
|
host,
|
|
2147
2242
|
...portFlag !== void 0 ? { port: Number(portFlag) } : {}
|
|
2148
2243
|
});
|
|
@@ -2161,7 +2256,7 @@ async function main() {
|
|
|
2161
2256
|
);
|
|
2162
2257
|
return;
|
|
2163
2258
|
}
|
|
2164
|
-
const transport = new
|
|
2259
|
+
const transport = new StdioServerTransport2();
|
|
2165
2260
|
await server.connect(transport);
|
|
2166
2261
|
}
|
|
2167
2262
|
main().catch((err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "virlow-mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.0",
|
|
4
4
|
"description": "Local MCP server for Virlow Secure Notes: end-to-end-encrypted AI memories and notes for Cursor, Claude, Codex, and any MCP client.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"esbuild": "^0.25.0",
|
|
35
35
|
"typescript": "^5.8.0",
|
|
36
36
|
"vitest": "^3.0.0",
|
|
37
|
-
"@batalabs/virlow-crypto": "3.
|
|
38
|
-
"@batalabs/virlow-memory": "3.
|
|
39
|
-
"@batalabs/virlow-mcp-core": "3.
|
|
37
|
+
"@batalabs/virlow-crypto": "3.19.0",
|
|
38
|
+
"@batalabs/virlow-memory": "3.19.0",
|
|
39
|
+
"@batalabs/virlow-mcp-core": "3.19.0"
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"build": "node build.mjs",
|