@rubytech/create-realagent 1.0.628 → 1.0.630
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/dist/index.js +34 -0
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.d.ts +26 -0
- package/payload/platform/lib/graph-mcp/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/index.js +193 -0
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-mcp/src/index.ts +225 -0
- package/payload/platform/lib/graph-mcp/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +4 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +4 -1
- package/payload/platform/plugins/docs/references/memory-guide.md +8 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/accept.sh +129 -0
- package/payload/platform/plugins/memory/mcp/scripts/graph/fixture.cypher +59 -0
- package/payload/platform/plugins/memory/references/graph-primitives.md +195 -0
- package/payload/server/public/assets/admin-DirN63aF.js +352 -0
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +249 -6
- package/payload/server/public/assets/admin-CGIu9HnV.js +0 -352
package/dist/index.js
CHANGED
|
@@ -813,6 +813,39 @@ function installOllama(embedModel) {
|
|
|
813
813
|
logFile(` Pulling embedding model: ${embedModel}`);
|
|
814
814
|
shellRetry("ollama", ["pull", embedModel], { timeout: 600_000 }, 3, 15);
|
|
815
815
|
}
|
|
816
|
+
// Task 557 — uv/uvx bootstrap. Required by the `graph` MCP server which
|
|
817
|
+
// spawns `uvx mcp-neo4j-cypher@0.6.0 --transport stdio`. Idempotent: when
|
|
818
|
+
// uvx is already on PATH (or under $HOME/.local/bin) we skip. Non-fatal on
|
|
819
|
+
// failure — the installer continues; the graph server will loudly fail
|
|
820
|
+
// at session start with a clear "uvx not found" error, which is retriable
|
|
821
|
+
// via a second installer run with network access.
|
|
822
|
+
function installUv() {
|
|
823
|
+
if (commandExists("uvx")) {
|
|
824
|
+
logFile(" uv: already installed");
|
|
825
|
+
console.log(" uv/uvx already installed.");
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log(" Installing uv (Python tool runner — required by Neo4j MCP server)...");
|
|
829
|
+
logFile(" uv: installing via astral.sh installer");
|
|
830
|
+
const result = spawnSync("bash", ["-c", "curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y"], { stdio: "inherit" });
|
|
831
|
+
if (result.status !== 0) {
|
|
832
|
+
console.error(` WARNING: uv install exited ${result.status} — graph MCP server will fail at session start until this is retried`);
|
|
833
|
+
logFile(` WARNING: uv install failed with status ${result.status}`);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
// The installer writes uvx to $HOME/.local/bin — add it to PATH for the
|
|
837
|
+
// remainder of this install so commandExists("uvx") works downstream.
|
|
838
|
+
const localBin = `${process.env.HOME}/.local/bin`;
|
|
839
|
+
if (process.env.PATH && !process.env.PATH.includes(localBin)) {
|
|
840
|
+
process.env.PATH = `${localBin}:${process.env.PATH}`;
|
|
841
|
+
}
|
|
842
|
+
if (commandExists("uvx")) {
|
|
843
|
+
logFile(" uv: install succeeded; uvx on PATH");
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
console.error(" WARNING: uv installed but uvx not on PATH — check $HOME/.local/bin");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
816
849
|
function installCloudflared() {
|
|
817
850
|
if (commandExists("cloudflared")) {
|
|
818
851
|
log("6", TOTAL, "Cloudflared already installed.");
|
|
@@ -1937,6 +1970,7 @@ try {
|
|
|
1937
1970
|
installNeo4j();
|
|
1938
1971
|
setupDedicatedNeo4j();
|
|
1939
1972
|
installOllama(EMBED_MODEL);
|
|
1973
|
+
installUv();
|
|
1940
1974
|
installCloudflared();
|
|
1941
1975
|
installWhisperCpp();
|
|
1942
1976
|
deployPayload(); // Must happen before ensureNeo4jPassword — restores config backup
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
4
|
+
*
|
|
5
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
6
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
7
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
8
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
9
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
10
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
11
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
12
|
+
* per-conversation stream log alongside every other plugin.
|
|
13
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
14
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
15
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
16
|
+
* start time per `id`, and on the response path to emit one
|
|
17
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
18
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
19
|
+
* observes, never mutates.
|
|
20
|
+
*
|
|
21
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
22
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
23
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
24
|
+
*/
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;GAsBG"}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
5
|
+
*
|
|
6
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
7
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
8
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
9
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
10
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
11
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
12
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
13
|
+
* per-conversation stream log alongside every other plugin.
|
|
14
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
15
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
16
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
17
|
+
* start time per `id`, and on the response path to emit one
|
|
18
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
19
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
20
|
+
* observes, never mutates.
|
|
21
|
+
*
|
|
22
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
23
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
24
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
const node_child_process_1 = require("node:child_process");
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const node_path_1 = require("node:path");
|
|
30
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
31
|
+
const index_js_1 = require("../../mcp-stderr-tee/dist/index.js");
|
|
32
|
+
const SERVER_NAME = "graph";
|
|
33
|
+
const UPSTREAM_PACKAGE = "mcp-neo4j-cypher@0.6.0";
|
|
34
|
+
(0, index_js_1.initStderrTee)(SERVER_NAME);
|
|
35
|
+
function resolvePassword() {
|
|
36
|
+
if (process.env.NEO4J_PASSWORD)
|
|
37
|
+
return process.env.NEO4J_PASSWORD;
|
|
38
|
+
// __dirname points at `lib/graph-mcp/dist` after compilation; the platform
|
|
39
|
+
// root is three levels up (lib/graph-mcp/dist -> lib/graph-mcp -> lib -> platform).
|
|
40
|
+
const root = process.env.PLATFORM_ROOT ?? (0, node_path_1.resolve)(__dirname, "../../..");
|
|
41
|
+
const file = (0, node_path_1.resolve)(root, "config/.neo4j-password");
|
|
42
|
+
try {
|
|
43
|
+
return (0, node_fs_1.readFileSync)(file, "utf-8").trim();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
console.error(`[graph-mcp] password unavailable — file=${file} env=NEO4J_PASSWORD (neither set)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const brand = process.env.BRAND ?? "maxy";
|
|
51
|
+
const neo4jUri = process.env.NEO4J_URI ?? "bolt://localhost:7687";
|
|
52
|
+
const neo4jUser = process.env.NEO4J_USERNAME ?? process.env.NEO4J_USER ?? "neo4j";
|
|
53
|
+
const neo4jPassword = resolvePassword();
|
|
54
|
+
const portMatch = /:(\d+)$/.exec(neo4jUri);
|
|
55
|
+
const neo4jPort = portMatch ? portMatch[1] : "?";
|
|
56
|
+
const namespace = process.env.NEO4J_NAMESPACE ?? "maxy-graph";
|
|
57
|
+
const readOnly = process.env.NEO4J_READ_ONLY ?? "true";
|
|
58
|
+
const responseTokenLimit = process.env.NEO4J_RESPONSE_TOKEN_LIMIT ?? "20000";
|
|
59
|
+
const childEnv = {
|
|
60
|
+
...process.env,
|
|
61
|
+
NEO4J_URI: neo4jUri,
|
|
62
|
+
NEO4J_URL: neo4jUri,
|
|
63
|
+
NEO4J_USERNAME: neo4jUser,
|
|
64
|
+
NEO4J_PASSWORD: neo4jPassword,
|
|
65
|
+
NEO4J_READ_ONLY: readOnly,
|
|
66
|
+
NEO4J_NAMESPACE: namespace,
|
|
67
|
+
NEO4J_RESPONSE_TOKEN_LIMIT: responseTokenLimit,
|
|
68
|
+
};
|
|
69
|
+
console.error(`[graph-mcp] boot brand=${brand} uri=${neo4jUri} user=${neo4jUser} ` +
|
|
70
|
+
`namespace=${namespace} readOnly=${readOnly} tokenLimit=${responseTokenLimit}`);
|
|
71
|
+
const child = (0, node_child_process_1.spawn)("uvx", [UPSTREAM_PACKAGE, "--transport", "stdio"], {
|
|
72
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
73
|
+
env: childEnv,
|
|
74
|
+
});
|
|
75
|
+
child.on("spawn", () => {
|
|
76
|
+
console.error(`[graph-mcp] spawned ${UPSTREAM_PACKAGE} pid=${child.pid}`);
|
|
77
|
+
});
|
|
78
|
+
child.on("error", (err) => {
|
|
79
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
80
|
+
console.error(`[graph-mcp] spawn error: ${msg}`);
|
|
81
|
+
if (err.code === "ENOENT") {
|
|
82
|
+
console.error("[graph-mcp] uvx not found on PATH — install uv: " +
|
|
83
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y");
|
|
84
|
+
}
|
|
85
|
+
process.exit(127);
|
|
86
|
+
});
|
|
87
|
+
child.on("exit", (code, signal) => {
|
|
88
|
+
console.error(`[graph-mcp] uvx exited code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
89
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
90
|
+
});
|
|
91
|
+
// Forward child stderr to our own stderr so the stderr tee picks it up.
|
|
92
|
+
child.stderr.on("data", (chunk) => {
|
|
93
|
+
process.stderr.write(chunk);
|
|
94
|
+
});
|
|
95
|
+
const pending = new Map();
|
|
96
|
+
function truncate(s, n) {
|
|
97
|
+
return s.length <= n ? s : `${s.slice(0, n)}...`;
|
|
98
|
+
}
|
|
99
|
+
function stripNamespace(toolName) {
|
|
100
|
+
if (!toolName)
|
|
101
|
+
return "(unknown)";
|
|
102
|
+
const prefix = `${namespace}_`;
|
|
103
|
+
return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
|
|
104
|
+
}
|
|
105
|
+
function extractCypher(args) {
|
|
106
|
+
if (!args)
|
|
107
|
+
return null;
|
|
108
|
+
const q = args["query"] ?? args["cypher"];
|
|
109
|
+
return typeof q === "string" ? truncate(q.replace(/\s+/g, " ").trim(), 80) : null;
|
|
110
|
+
}
|
|
111
|
+
function countRows(result) {
|
|
112
|
+
if (!result?.content || !Array.isArray(result.content))
|
|
113
|
+
return "?";
|
|
114
|
+
const text = result.content[0]?.text ?? "";
|
|
115
|
+
const rowsMatch = /(\d+)\s+(?:rows?|records?)/i.exec(text);
|
|
116
|
+
if (rowsMatch)
|
|
117
|
+
return rowsMatch[1];
|
|
118
|
+
return String(result.content.length);
|
|
119
|
+
}
|
|
120
|
+
function onRequestLine(line) {
|
|
121
|
+
try {
|
|
122
|
+
const msg = JSON.parse(line);
|
|
123
|
+
if (msg.method === "tools/call" && msg.id !== undefined) {
|
|
124
|
+
pending.set(msg.id, {
|
|
125
|
+
method: stripNamespace(msg.params?.name),
|
|
126
|
+
cypherPrefix: extractCypher(msg.params?.arguments),
|
|
127
|
+
startMs: Date.now(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Non-JSON / partial line — forward untouched, don't log.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function onResponseLine(line) {
|
|
136
|
+
try {
|
|
137
|
+
const msg = JSON.parse(line);
|
|
138
|
+
if (msg.id === undefined || !pending.has(msg.id))
|
|
139
|
+
return;
|
|
140
|
+
const p = pending.get(msg.id);
|
|
141
|
+
pending.delete(msg.id);
|
|
142
|
+
const elapsed = Date.now() - p.startMs;
|
|
143
|
+
const cypherField = p.cypherPrefix ? `cypher="${p.cypherPrefix.replace(/"/g, "'")}"` : "";
|
|
144
|
+
if (msg.error) {
|
|
145
|
+
const errText = (msg.error.message ?? JSON.stringify(msg.error)).replace(/"/g, "'");
|
|
146
|
+
console.error(`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} error="${errText}" ms=${elapsed}`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const rows = countRows(msg.result);
|
|
150
|
+
console.error(`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} rows=${rows} ms=${elapsed}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Non-JSON — forward untouched.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function makeLineSplitter(onLine) {
|
|
158
|
+
const decoder = new node_string_decoder_1.StringDecoder("utf8");
|
|
159
|
+
let buf = "";
|
|
160
|
+
return (chunk) => {
|
|
161
|
+
buf += decoder.write(chunk);
|
|
162
|
+
let idx;
|
|
163
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
164
|
+
const line = buf.slice(0, idx);
|
|
165
|
+
buf = buf.slice(idx + 1);
|
|
166
|
+
if (line.length > 0)
|
|
167
|
+
onLine(line);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const splitRequest = makeLineSplitter(onRequestLine);
|
|
172
|
+
process.stdin.on("data", (chunk) => {
|
|
173
|
+
splitRequest(chunk);
|
|
174
|
+
child.stdin.write(chunk);
|
|
175
|
+
});
|
|
176
|
+
process.stdin.on("end", () => {
|
|
177
|
+
child.stdin.end();
|
|
178
|
+
});
|
|
179
|
+
const splitResponse = makeLineSplitter(onResponseLine);
|
|
180
|
+
child.stdout.on("data", (chunk) => {
|
|
181
|
+
splitResponse(chunk);
|
|
182
|
+
process.stdout.write(chunk);
|
|
183
|
+
});
|
|
184
|
+
child.stdout.on("end", () => {
|
|
185
|
+
process.stdout.end();
|
|
186
|
+
});
|
|
187
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
188
|
+
process.on(sig, () => {
|
|
189
|
+
console.error(`[graph-mcp] ${sig} received — forwarding to child`);
|
|
190
|
+
child.kill(sig);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Graph MCP shim — adopts upstream `mcp-neo4j-cypher` per brand.
|
|
4
|
+
*
|
|
5
|
+
* Task 557. Claude Code spawns this file as the `graph` MCP server; it
|
|
6
|
+
* starts the upstream Python server under `uvx` and proxies the JSON-RPC
|
|
7
|
+
* stdio stream byte-for-byte. While proxying, it:
|
|
8
|
+
* 1. Resolves `NEO4J_URI/USERNAME/PASSWORD` from the brand's own env
|
|
9
|
+
* (falls back to `${PLATFORM_ROOT}/config/.neo4j-password`, matching
|
|
10
|
+
* plugins/memory/mcp/src/lib/neo4j.ts:10-21).
|
|
11
|
+
* 2. Installs the Maxy stderr tee so upstream stderr lands in the
|
|
12
|
+
* per-conversation stream log alongside every other plugin.
|
|
13
|
+
* 3. Locks the upstream server into read-only mode (`NEO4J_READ_ONLY=true`)
|
|
14
|
+
* and a stable Maxy namespace (`NEO4J_NAMESPACE=maxy-graph`).
|
|
15
|
+
* 4. Parses JSON-RPC lines on the request path to record the cypher and
|
|
16
|
+
* start time per `id`, and on the response path to emit one
|
|
17
|
+
* `[graph-query] op=... brand=... port=... cypher="..." rows=... ms=...`
|
|
18
|
+
* line per tool call. Forwarding is byte-accurate — the parser
|
|
19
|
+
* observes, never mutates.
|
|
20
|
+
*
|
|
21
|
+
* Isolation model per MAXY-PRD.md:627 and create-maxy src/index.ts:504-824:
|
|
22
|
+
* each brand already owns its own Neo4j instance on its own port with its
|
|
23
|
+
* own password. This shim trusts that — no query-layer tenant filtering.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { spawn } from "node:child_process";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
|
+
import { resolve } from "node:path";
|
|
29
|
+
import { StringDecoder } from "node:string_decoder";
|
|
30
|
+
import { initStderrTee } from "../../mcp-stderr-tee/dist/index.js";
|
|
31
|
+
|
|
32
|
+
const SERVER_NAME = "graph";
|
|
33
|
+
const UPSTREAM_PACKAGE = "mcp-neo4j-cypher@0.6.0";
|
|
34
|
+
|
|
35
|
+
initStderrTee(SERVER_NAME);
|
|
36
|
+
|
|
37
|
+
function resolvePassword(): string {
|
|
38
|
+
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
39
|
+
// __dirname points at `lib/graph-mcp/dist` after compilation; the platform
|
|
40
|
+
// root is three levels up (lib/graph-mcp/dist -> lib/graph-mcp -> lib -> platform).
|
|
41
|
+
const root = process.env.PLATFORM_ROOT ?? resolve(__dirname, "../../..");
|
|
42
|
+
const file = resolve(root, "config/.neo4j-password");
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(file, "utf-8").trim();
|
|
45
|
+
} catch {
|
|
46
|
+
console.error(`[graph-mcp] password unavailable — file=${file} env=NEO4J_PASSWORD (neither set)`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const brand = process.env.BRAND ?? "maxy";
|
|
52
|
+
const neo4jUri = process.env.NEO4J_URI ?? "bolt://localhost:7687";
|
|
53
|
+
const neo4jUser = process.env.NEO4J_USERNAME ?? process.env.NEO4J_USER ?? "neo4j";
|
|
54
|
+
const neo4jPassword = resolvePassword();
|
|
55
|
+
const portMatch = /:(\d+)$/.exec(neo4jUri);
|
|
56
|
+
const neo4jPort = portMatch ? portMatch[1] : "?";
|
|
57
|
+
const namespace = process.env.NEO4J_NAMESPACE ?? "maxy-graph";
|
|
58
|
+
const readOnly = process.env.NEO4J_READ_ONLY ?? "true";
|
|
59
|
+
const responseTokenLimit = process.env.NEO4J_RESPONSE_TOKEN_LIMIT ?? "20000";
|
|
60
|
+
|
|
61
|
+
const childEnv: NodeJS.ProcessEnv = {
|
|
62
|
+
...process.env,
|
|
63
|
+
NEO4J_URI: neo4jUri,
|
|
64
|
+
NEO4J_URL: neo4jUri,
|
|
65
|
+
NEO4J_USERNAME: neo4jUser,
|
|
66
|
+
NEO4J_PASSWORD: neo4jPassword,
|
|
67
|
+
NEO4J_READ_ONLY: readOnly,
|
|
68
|
+
NEO4J_NAMESPACE: namespace,
|
|
69
|
+
NEO4J_RESPONSE_TOKEN_LIMIT: responseTokenLimit,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
console.error(
|
|
73
|
+
`[graph-mcp] boot brand=${brand} uri=${neo4jUri} user=${neo4jUser} ` +
|
|
74
|
+
`namespace=${namespace} readOnly=${readOnly} tokenLimit=${responseTokenLimit}`,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const child = spawn("uvx", [UPSTREAM_PACKAGE, "--transport", "stdio"], {
|
|
78
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
79
|
+
env: childEnv,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on("spawn", () => {
|
|
83
|
+
console.error(`[graph-mcp] spawned ${UPSTREAM_PACKAGE} pid=${child.pid}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
child.on("error", (err) => {
|
|
87
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
88
|
+
console.error(`[graph-mcp] spawn error: ${msg}`);
|
|
89
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
90
|
+
console.error(
|
|
91
|
+
"[graph-mcp] uvx not found on PATH — install uv: " +
|
|
92
|
+
"curl -LsSf https://astral.sh/uv/install.sh | sh -s -- -y",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
process.exit(127);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
child.on("exit", (code, signal) => {
|
|
99
|
+
console.error(`[graph-mcp] uvx exited code=${code ?? "null"} signal=${signal ?? "null"}`);
|
|
100
|
+
process.exit(code ?? (signal ? 1 : 0));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Forward child stderr to our own stderr so the stderr tee picks it up.
|
|
104
|
+
child.stderr.on("data", (chunk: Buffer) => {
|
|
105
|
+
process.stderr.write(chunk);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// --- JSON-RPC call correlation ---
|
|
109
|
+
// tools/call is the only method we time; initialize and tools/list are noise.
|
|
110
|
+
interface PendingCall {
|
|
111
|
+
method: string;
|
|
112
|
+
cypherPrefix: string | null;
|
|
113
|
+
startMs: number;
|
|
114
|
+
}
|
|
115
|
+
const pending = new Map<string | number, PendingCall>();
|
|
116
|
+
|
|
117
|
+
function truncate(s: string, n: number): string {
|
|
118
|
+
return s.length <= n ? s : `${s.slice(0, n)}...`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function stripNamespace(toolName: string | undefined): string {
|
|
122
|
+
if (!toolName) return "(unknown)";
|
|
123
|
+
const prefix = `${namespace}_`;
|
|
124
|
+
return toolName.startsWith(prefix) ? toolName.slice(prefix.length) : toolName;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type JsonRpcMessage = {
|
|
128
|
+
id?: string | number;
|
|
129
|
+
method?: string;
|
|
130
|
+
params?: { name?: string; arguments?: Record<string, unknown> };
|
|
131
|
+
result?: { content?: Array<{ text?: string; type?: string }> };
|
|
132
|
+
error?: { message?: string; code?: number };
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function extractCypher(args: Record<string, unknown> | undefined): string | null {
|
|
136
|
+
if (!args) return null;
|
|
137
|
+
const q = args["query"] ?? args["cypher"];
|
|
138
|
+
return typeof q === "string" ? truncate(q.replace(/\s+/g, " ").trim(), 80) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function countRows(result: JsonRpcMessage["result"]): string {
|
|
142
|
+
if (!result?.content || !Array.isArray(result.content)) return "?";
|
|
143
|
+
const text = result.content[0]?.text ?? "";
|
|
144
|
+
const rowsMatch = /(\d+)\s+(?:rows?|records?)/i.exec(text);
|
|
145
|
+
if (rowsMatch) return rowsMatch[1];
|
|
146
|
+
return String(result.content.length);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onRequestLine(line: string): void {
|
|
150
|
+
try {
|
|
151
|
+
const msg = JSON.parse(line) as JsonRpcMessage;
|
|
152
|
+
if (msg.method === "tools/call" && msg.id !== undefined) {
|
|
153
|
+
pending.set(msg.id, {
|
|
154
|
+
method: stripNamespace(msg.params?.name),
|
|
155
|
+
cypherPrefix: extractCypher(msg.params?.arguments),
|
|
156
|
+
startMs: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Non-JSON / partial line — forward untouched, don't log.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function onResponseLine(line: string): void {
|
|
165
|
+
try {
|
|
166
|
+
const msg = JSON.parse(line) as JsonRpcMessage;
|
|
167
|
+
if (msg.id === undefined || !pending.has(msg.id)) return;
|
|
168
|
+
const p = pending.get(msg.id)!;
|
|
169
|
+
pending.delete(msg.id);
|
|
170
|
+
const elapsed = Date.now() - p.startMs;
|
|
171
|
+
const cypherField = p.cypherPrefix ? `cypher="${p.cypherPrefix.replace(/"/g, "'")}"` : "";
|
|
172
|
+
if (msg.error) {
|
|
173
|
+
const errText = (msg.error.message ?? JSON.stringify(msg.error)).replace(/"/g, "'");
|
|
174
|
+
console.error(
|
|
175
|
+
`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} error="${errText}" ms=${elapsed}`,
|
|
176
|
+
);
|
|
177
|
+
} else {
|
|
178
|
+
const rows = countRows(msg.result);
|
|
179
|
+
console.error(
|
|
180
|
+
`[graph-query] op=${p.method} brand=${brand} port=${neo4jPort} ${cypherField} rows=${rows} ms=${elapsed}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Non-JSON — forward untouched.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function makeLineSplitter(onLine: (line: string) => void): (chunk: Buffer) => void {
|
|
189
|
+
const decoder = new StringDecoder("utf8");
|
|
190
|
+
let buf = "";
|
|
191
|
+
return (chunk: Buffer) => {
|
|
192
|
+
buf += decoder.write(chunk);
|
|
193
|
+
let idx: number;
|
|
194
|
+
while ((idx = buf.indexOf("\n")) !== -1) {
|
|
195
|
+
const line = buf.slice(0, idx);
|
|
196
|
+
buf = buf.slice(idx + 1);
|
|
197
|
+
if (line.length > 0) onLine(line);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const splitRequest = makeLineSplitter(onRequestLine);
|
|
203
|
+
process.stdin.on("data", (chunk: Buffer) => {
|
|
204
|
+
splitRequest(chunk);
|
|
205
|
+
child.stdin.write(chunk);
|
|
206
|
+
});
|
|
207
|
+
process.stdin.on("end", () => {
|
|
208
|
+
child.stdin.end();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const splitResponse = makeLineSplitter(onResponseLine);
|
|
212
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
213
|
+
splitResponse(chunk);
|
|
214
|
+
process.stdout.write(chunk);
|
|
215
|
+
});
|
|
216
|
+
child.stdout.on("end", () => {
|
|
217
|
+
process.stdout.end();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
|
|
221
|
+
process.on(sig, () => {
|
|
222
|
+
console.error(`[graph-mcp] ${sig} received — forwarding to child`);
|
|
223
|
+
child.kill(sig);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -27,7 +27,10 @@ set -euo pipefail
|
|
|
27
27
|
# --------------------------------------------------------------------------
|
|
28
28
|
|
|
29
29
|
# shellcheck source=_stream-log.sh
|
|
30
|
-
|
|
30
|
+
# Resolve symlinks before dirname — ~/reset-tunnel.sh is installed as a symlink
|
|
31
|
+
# (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
|
|
32
|
+
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
33
|
+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
31
34
|
require_stream_log_path reset-tunnel
|
|
32
35
|
|
|
33
36
|
if [ "$#" -lt 1 ]; then
|
|
@@ -29,7 +29,10 @@ set -euo pipefail
|
|
|
29
29
|
# --------------------------------------------------------------------------
|
|
30
30
|
|
|
31
31
|
# shellcheck source=_stream-log.sh
|
|
32
|
-
|
|
32
|
+
# Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
|
|
33
|
+
# (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
|
|
34
|
+
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
35
|
+
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
33
36
|
require_stream_log_path setup-tunnel
|
|
34
37
|
|
|
35
38
|
# --------------------------------------------------------------------------
|
|
@@ -80,6 +80,14 @@ Ask naturally:
|
|
|
80
80
|
- "What did I last discuss about the Acme proposal?"
|
|
81
81
|
- "Who have I met from the fintech conference?"
|
|
82
82
|
|
|
83
|
+
## Listing and counting (Task 557)
|
|
84
|
+
|
|
85
|
+
Maxy answers relational questions — "list all my people", "how many tasks do I have", "find the person with email X", "show me the 20 most recently created nodes" — via direct read-only Cypher against your Neo4j. This is faster and more precise than semantic search when the question is "the exact set where", not "things similar to".
|
|
86
|
+
|
|
87
|
+
You can also open the Neo4j Browser at any time from the burger menu → **Graph**. Sign in with your Neo4j username (`neo4j`) and password (stored in `config/.neo4j-password` on the device). Run `MATCH (n) RETURN n LIMIT 25` for a visual overview of your graph, or write your own Cypher for ad-hoc exploration.
|
|
88
|
+
|
|
89
|
+
The browser reaches only your own brand's Neo4j — a Maxy device and a Real Agent device share no graph state even when on the same laptop.
|
|
90
|
+
|
|
83
91
|
## Privacy
|
|
84
92
|
|
|
85
93
|
All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. Maxy does not sync memory to any cloud service or third party.
|