@dbx-tools/appkit-graphiti 0.6.114 → 0.6.115
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 +47 -33
- package/lib/src/config.d.ts +6 -20
- package/lib/src/config.js +9 -27
- package/lib/src/plugin.d.ts +21 -9
- package/lib/src/plugin.js +247 -94
- package/package.json +7 -5
- package/src/config.ts +10 -46
- package/src/plugin.ts +285 -95
package/src/config.ts
CHANGED
|
@@ -1,28 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AppKit Graphiti sidecar ports
|
|
2
|
+
* AppKit Graphiti sidecar ports and Python command.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { ConfigurationError, type BasePluginConfig } from "@databricks/appkit";
|
|
7
|
-
import { config as coreConfig } from "@dbx-tools/core";
|
|
7
|
+
import { config as coreConfig, project as coreProject } from "@dbx-tools/core";
|
|
8
8
|
import type { JSONSchema7 } from "json-schema";
|
|
9
9
|
|
|
10
10
|
export interface GraphitiPluginConfig extends BasePluginConfig {
|
|
11
|
-
publicPort?: number;
|
|
12
|
-
appPort?: number;
|
|
13
11
|
graphitiPort?: number;
|
|
14
12
|
litellmPort?: number;
|
|
15
|
-
|
|
13
|
+
proxyPort?: number;
|
|
16
14
|
python?: string;
|
|
17
15
|
journalNamespace?: string;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
|
|
21
|
-
publicPort: number;
|
|
22
|
-
appPort: number;
|
|
23
19
|
graphitiPort: number;
|
|
24
20
|
litellmPort: number;
|
|
25
|
-
|
|
21
|
+
proxyPort: number;
|
|
26
22
|
python: string;
|
|
27
23
|
journalNamespace: string;
|
|
28
24
|
}
|
|
@@ -30,11 +26,9 @@ export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
|
|
|
30
26
|
export const GRAPHITI_CONFIG_SCHEMA = {
|
|
31
27
|
type: "object",
|
|
32
28
|
properties: {
|
|
33
|
-
publicPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
34
|
-
appPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
35
29
|
graphitiPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
36
30
|
litellmPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
37
|
-
|
|
31
|
+
proxyPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
38
32
|
python: { type: "string" },
|
|
39
33
|
journalNamespace: { type: "string" },
|
|
40
34
|
},
|
|
@@ -45,18 +39,6 @@ export const GRAPHITI_CONFIG_SCHEMA = {
|
|
|
45
39
|
export function resolveGraphitiConfig(
|
|
46
40
|
config: GraphitiPluginConfig = {},
|
|
47
41
|
): ResolvedGraphitiPluginConfig {
|
|
48
|
-
const publicPort = coreConfig.port(
|
|
49
|
-
config.publicPort,
|
|
50
|
-
"DATABRICKS_APP_PORT",
|
|
51
|
-
8000,
|
|
52
|
-
coreConfig.ENV_ONLY,
|
|
53
|
-
);
|
|
54
|
-
const appPort = coreConfig.port(
|
|
55
|
-
config.appPort,
|
|
56
|
-
"GRAPHITI_APP_PORT",
|
|
57
|
-
publicPort + 1,
|
|
58
|
-
coreConfig.ENV_ONLY,
|
|
59
|
-
);
|
|
60
42
|
const graphitiPort = coreConfig.port(
|
|
61
43
|
config.graphitiPort,
|
|
62
44
|
"GRAPHITI_PORT",
|
|
@@ -64,42 +46,24 @@ export function resolveGraphitiConfig(
|
|
|
64
46
|
coreConfig.ENV_ONLY,
|
|
65
47
|
);
|
|
66
48
|
const litellmPort = coreConfig.port(config.litellmPort, "LITELLM_PORT", 0, coreConfig.ENV_ONLY);
|
|
67
|
-
const
|
|
49
|
+
const proxyPort = coreConfig.port(config.proxyPort, "PROXY_PORT", 0, coreConfig.ENV_ONLY);
|
|
50
|
+
const configuredPorts = [graphitiPort, litellmPort, proxyPort].filter(Boolean);
|
|
68
51
|
if (new Set(configuredPorts).size !== configuredPorts.length) {
|
|
69
|
-
throw new ConfigurationError("Graphiti
|
|
52
|
+
throw new ConfigurationError("Graphiti sidecar ports must be distinct");
|
|
70
53
|
}
|
|
71
|
-
const routePrefix = normalizeRoutePrefix(
|
|
72
|
-
config.routePrefix ??
|
|
73
|
-
coreConfig.text("GRAPHITI_ROUTE_PREFIX", coreConfig.ENV_ONLY) ??
|
|
74
|
-
"/graphiti",
|
|
75
|
-
);
|
|
76
54
|
const python =
|
|
77
55
|
config.python ?? coreConfig.text("PYTHON", coreConfig.ENV_ONLY)?.trim() ?? "python3";
|
|
78
56
|
const journalNamespace =
|
|
79
57
|
config.journalNamespace ??
|
|
80
58
|
coreConfig.text("JOURNAL_NAMESPACE", coreConfig.ENV_ONLY)?.trim() ??
|
|
81
59
|
process.env.DATABRICKS_APP_NAME?.trim() ??
|
|
60
|
+
coreProject.name() ??
|
|
82
61
|
"default";
|
|
83
62
|
return {
|
|
84
|
-
publicPort,
|
|
85
|
-
appPort,
|
|
86
63
|
graphitiPort,
|
|
87
64
|
litellmPort,
|
|
88
|
-
|
|
65
|
+
proxyPort,
|
|
89
66
|
python,
|
|
90
67
|
journalNamespace,
|
|
91
68
|
};
|
|
92
69
|
}
|
|
93
|
-
|
|
94
|
-
/** Return the internal port an AppKit `server()` plugin should bind. */
|
|
95
|
-
export function appPort(config: GraphitiPluginConfig = {}): number {
|
|
96
|
-
return resolveGraphitiConfig(config).appPort;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function normalizeRoutePrefix(value: string): string {
|
|
100
|
-
const normalized = `/${value.trim().replace(/^\/+|\/+$/g, "")}`;
|
|
101
|
-
if (normalized === "/") {
|
|
102
|
-
throw new ConfigurationError("Graphiti routePrefix must contain a path segment");
|
|
103
|
-
}
|
|
104
|
-
return normalized;
|
|
105
|
-
}
|
package/src/plugin.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AppKit plugin that supervises Graphiti and
|
|
2
|
+
* AppKit plugin that supervises Graphiti and republishes user-scoped MCP tools.
|
|
3
3
|
*
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import { createServer } from "node:net";
|
|
7
8
|
import {
|
|
9
|
+
ConfigurationError,
|
|
8
10
|
Plugin,
|
|
9
11
|
lakebase,
|
|
10
12
|
toPlugin,
|
|
13
|
+
type IAppRouter,
|
|
11
14
|
type PluginManifest,
|
|
12
15
|
type ResourceRequirement,
|
|
13
16
|
} from "@databricks/appkit";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import {
|
|
18
|
+
appkit as dbxAppkit,
|
|
19
|
+
identity as appkitIdentity,
|
|
20
|
+
plugin as appkitPlugin,
|
|
21
|
+
} from "@dbx-tools/appkit";
|
|
22
|
+
import { config as coreConfig } from "@dbx-tools/core";
|
|
23
|
+
import { async as asyncModule, log, object } from "@dbx-tools/shared-core";
|
|
24
|
+
import { createTool, type Tool } from "@mastra/core/tools";
|
|
25
|
+
import { MCPClient, MCPServer } from "@mastra/mcp";
|
|
26
|
+
import concurrently, { type Command, type ConcurrentlyResult } from "concurrently";
|
|
27
|
+
import type express from "express";
|
|
19
28
|
import {
|
|
20
29
|
GRAPHITI_CONFIG_SCHEMA,
|
|
21
30
|
resolveGraphitiConfig,
|
|
@@ -24,8 +33,31 @@ import {
|
|
|
24
33
|
} from "./config.ts";
|
|
25
34
|
|
|
26
35
|
const LAKEBASE_MANIFEST = appkitPlugin.data(lakebase).plugin.manifest;
|
|
36
|
+
const MCP_PATH = "/api/graphiti/mcp";
|
|
37
|
+
const MCP_SERVER_IDLE_MS = 30 * 60 * 1000;
|
|
38
|
+
const MCP_SERVER_SWEEP_MS = 5 * 60 * 1000;
|
|
39
|
+
const SCOPED_TOOL_FIELDS = {
|
|
40
|
+
add_memory: "group_id",
|
|
41
|
+
add_triplet: "group_id",
|
|
42
|
+
build_communities: "group_ids",
|
|
43
|
+
get_episodes: "group_ids",
|
|
44
|
+
get_status: undefined,
|
|
45
|
+
search_memory_facts: "group_ids",
|
|
46
|
+
search_nodes: "group_ids",
|
|
47
|
+
summarize_saga: "group_id",
|
|
48
|
+
} as const;
|
|
49
|
+
const WRITE_TOOLS = new Set(["add_memory", "add_triplet", "build_communities", "summarize_saga"]);
|
|
50
|
+
const UNSCOPED_ARGUMENTS = [
|
|
51
|
+
"center_node_uuid",
|
|
52
|
+
"previous_episode_uuids",
|
|
53
|
+
"saga_previous_episode_uuid",
|
|
54
|
+
"source_node_uuid",
|
|
55
|
+
"target_node_uuid",
|
|
56
|
+
"uuid",
|
|
57
|
+
] as const;
|
|
27
58
|
|
|
28
59
|
interface ToolkitEntry {
|
|
60
|
+
readonly __toolkitRef: true;
|
|
29
61
|
pluginName: string;
|
|
30
62
|
localName: string;
|
|
31
63
|
def: {
|
|
@@ -33,6 +65,15 @@ interface ToolkitEntry {
|
|
|
33
65
|
description: string;
|
|
34
66
|
parameters: unknown;
|
|
35
67
|
};
|
|
68
|
+
annotations: {
|
|
69
|
+
effect: "read" | "write";
|
|
70
|
+
requiresUserContext: true;
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface UserMcpServer {
|
|
75
|
+
lastUsed: number;
|
|
76
|
+
server: MCPServer;
|
|
36
77
|
}
|
|
37
78
|
|
|
38
79
|
export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
@@ -58,117 +99,144 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
|
58
99
|
}
|
|
59
100
|
|
|
60
101
|
private readonly logger = log.logger(this);
|
|
61
|
-
private
|
|
102
|
+
private commands: Command[] = [];
|
|
62
103
|
private mcp?: MCPClient;
|
|
104
|
+
private mcpServers = new Map<string, UserMcpServer>();
|
|
105
|
+
private mcpServerSweep?: NodeJS.Timeout;
|
|
63
106
|
private mcpTools: Record<string, Tool> = {};
|
|
64
107
|
private resolved?: ResolvedGraphitiPluginConfig;
|
|
108
|
+
private setupComplete = false;
|
|
109
|
+
private supervision?: ConcurrentlyResult;
|
|
65
110
|
private stopping = false;
|
|
66
111
|
|
|
67
112
|
override async setup(): Promise<void> {
|
|
68
113
|
const configured = resolveGraphitiConfig(this.config);
|
|
69
|
-
const graphitiPort
|
|
70
|
-
|
|
71
|
-
|
|
114
|
+
const [graphitiPort, litellmPort, proxyPort] = await distinctPorts(
|
|
115
|
+
coreConfig.port(undefined, "DATABRICKS_APP_PORT", 8000, coreConfig.ENV_ONLY),
|
|
116
|
+
configured.graphitiPort,
|
|
117
|
+
configured.litellmPort,
|
|
118
|
+
configured.proxyPort,
|
|
119
|
+
);
|
|
72
120
|
this.resolved = {
|
|
73
121
|
...configured,
|
|
74
122
|
graphitiPort,
|
|
75
123
|
litellmPort,
|
|
124
|
+
proxyPort,
|
|
76
125
|
};
|
|
77
|
-
|
|
78
|
-
throw new Error(
|
|
79
|
-
"Graphiti requires Lakebase; register lakebase() before graphiti() and bind its Postgres resource",
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
|
83
|
-
process.once(signal, () => this.stopGraphiti());
|
|
84
|
-
}
|
|
85
|
-
const supervisor = appkitInterceptor.createInterceptorContext({});
|
|
86
|
-
supervisor.context.onTeardown(() => this.stopGraphiti());
|
|
87
|
-
const graphiti = exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "start"], {
|
|
88
|
-
env: {
|
|
89
|
-
...process.env,
|
|
90
|
-
GRAPHITI_HOST: "127.0.0.1",
|
|
91
|
-
GRAPHITI_PORT: String(this.resolved.graphitiPort),
|
|
92
|
-
JOURNAL_NAMESPACE: this.resolved.journalNamespace,
|
|
93
|
-
LITELLM_HOST: "127.0.0.1",
|
|
94
|
-
LITELLM_PORT: String(this.resolved.litellmPort),
|
|
95
|
-
MANAGE_LITELLM: "true",
|
|
96
|
-
},
|
|
97
|
-
stdin: "ignore",
|
|
98
|
-
stdout: "inherit",
|
|
99
|
-
stderr: "inherit",
|
|
100
|
-
});
|
|
101
|
-
const caddy = exec.spawn(
|
|
102
|
-
this.resolved.python,
|
|
126
|
+
this.supervision = concurrently(
|
|
103
127
|
[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
128
|
+
{
|
|
129
|
+
name: "graphiti",
|
|
130
|
+
command: commandLine([this.resolved.python, "-m", "dbx_tools.graphiti", "start"]),
|
|
131
|
+
env: {
|
|
132
|
+
...process.env,
|
|
133
|
+
GRAPHITI_HOST: "127.0.0.1",
|
|
134
|
+
GRAPHITI_PORT: String(this.resolved.graphitiPort),
|
|
135
|
+
JOURNAL_NAMESPACE: this.resolved.journalNamespace,
|
|
136
|
+
LITELLM_HOST: "127.0.0.1",
|
|
137
|
+
LITELLM_PORT: String(this.resolved.litellmPort),
|
|
138
|
+
MANAGE_LITELLM: "true",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "caddy",
|
|
143
|
+
command: commandLine([
|
|
144
|
+
this.resolved.python,
|
|
145
|
+
"-m",
|
|
146
|
+
"dbx_tools.graphiti.proxy",
|
|
147
|
+
"--proxy-port",
|
|
148
|
+
String(this.resolved.proxyPort),
|
|
149
|
+
"--graphiti-port",
|
|
150
|
+
String(this.resolved.graphitiPort),
|
|
151
|
+
]),
|
|
152
|
+
env: process.env,
|
|
153
|
+
},
|
|
114
154
|
],
|
|
115
155
|
{
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
156
|
+
killOthersOn: ["failure", "success"],
|
|
157
|
+
killSignal: "SIGTERM",
|
|
158
|
+
killTimeout: 12_000,
|
|
159
|
+
prefix: "name",
|
|
160
|
+
prefixColors: false,
|
|
120
161
|
},
|
|
121
162
|
);
|
|
122
|
-
this.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
163
|
+
this.commands = this.supervision.commands;
|
|
164
|
+
void this.supervision.result.then(
|
|
165
|
+
() => this.onSupervisorExit(),
|
|
166
|
+
(error) => this.onSupervisorExit(error),
|
|
167
|
+
);
|
|
168
|
+
const supervisionFailure = this.supervision.result.then(
|
|
169
|
+
() => {
|
|
170
|
+
throw new Error("Graphiti sidecar supervisor exited during startup");
|
|
171
|
+
},
|
|
172
|
+
(error) => {
|
|
173
|
+
throw error;
|
|
131
174
|
},
|
|
132
|
-
});
|
|
133
|
-
const discovered = await this.mcp.listTools();
|
|
134
|
-
this.mcpTools = Object.fromEntries(
|
|
135
|
-
Object.entries(discovered).map(([name, tool]) => [
|
|
136
|
-
name.replace(/^graphiti_/, ""),
|
|
137
|
-
tool as Tool,
|
|
138
|
-
]),
|
|
139
175
|
);
|
|
176
|
+
try {
|
|
177
|
+
await Promise.race([
|
|
178
|
+
(async () => {
|
|
179
|
+
await waitForGraphiti(graphitiPort);
|
|
180
|
+
await waitForGraphiti(proxyPort);
|
|
181
|
+
})(),
|
|
182
|
+
supervisionFailure,
|
|
183
|
+
]);
|
|
184
|
+
this.mcp = new MCPClient({
|
|
185
|
+
id: `appkit-graphiti-${this.resolved.graphitiPort}`,
|
|
186
|
+
servers: {
|
|
187
|
+
graphiti: {
|
|
188
|
+
url: new URL(`http://127.0.0.1:${this.resolved.proxyPort}/mcp`),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const discovered = await this.mcp.listTools();
|
|
193
|
+
this.mcpTools = Object.fromEntries(
|
|
194
|
+
Object.entries(discovered)
|
|
195
|
+
.map(([name, tool]) => [name.replace(/^graphiti_/, ""), tool as Tool] as const)
|
|
196
|
+
.filter(([name]) => name in SCOPED_TOOL_FIELDS),
|
|
197
|
+
);
|
|
198
|
+
const missing = Object.keys(SCOPED_TOOL_FIELDS).filter((name) => !this.mcpTools[name]);
|
|
199
|
+
if (missing.length > 0) {
|
|
200
|
+
throw new Error(`Graphiti did not publish required scoped tools: ${missing.join(", ")}`);
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await this.stopSidecars();
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
this.setupComplete = true;
|
|
207
|
+
this.mcpServerSweep = setInterval(() => this.closeIdleMcpServers(), MCP_SERVER_SWEEP_MS);
|
|
208
|
+
this.mcpServerSweep.unref();
|
|
140
209
|
this.logger.info("ready", {
|
|
141
|
-
appPort: this.resolved.appPort,
|
|
142
210
|
graphitiPort: this.resolved.graphitiPort,
|
|
143
211
|
litellmPort: this.resolved.litellmPort,
|
|
144
|
-
|
|
145
|
-
mcpPath:
|
|
212
|
+
proxyPort: this.resolved.proxyPort,
|
|
213
|
+
mcpPath: MCP_PATH,
|
|
146
214
|
tools: Object.keys(this.mcpTools),
|
|
147
215
|
});
|
|
148
216
|
}
|
|
149
217
|
|
|
218
|
+
override injectRoutes(router: IAppRouter): void {
|
|
219
|
+
for (const method of ["get", "post", "delete"] as const) {
|
|
220
|
+
this.route(router, {
|
|
221
|
+
name: `${method}Mcp`,
|
|
222
|
+
method,
|
|
223
|
+
path: "/mcp",
|
|
224
|
+
handler: (request, response) => this.forwardMcp(request, response),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
150
229
|
override abortActiveOperations(): void {
|
|
151
230
|
super.abortActiveOperations();
|
|
152
|
-
this.stopGraphiti();
|
|
153
231
|
void this.mcp?.disconnect();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
this.children = [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async shutdown(): Promise<void> {
|
|
235
|
+
await this.stopSidecars();
|
|
160
236
|
}
|
|
161
237
|
|
|
162
238
|
override exports() {
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
appPort: resolved.appPort,
|
|
166
|
-
graphitiPort: resolved.graphitiPort,
|
|
167
|
-
litellmPort: resolved.litellmPort,
|
|
168
|
-
mcpPath: `${resolved.routePrefix}/mcp/`,
|
|
169
|
-
publicPort: resolved.publicPort,
|
|
170
|
-
routePrefix: resolved.routePrefix,
|
|
171
|
-
};
|
|
239
|
+
return { mcpPath: MCP_PATH };
|
|
172
240
|
}
|
|
173
241
|
|
|
174
242
|
toolkit(): Record<string, ToolkitEntry> {
|
|
@@ -176,6 +244,7 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
|
176
244
|
Object.entries(this.mcpTools).map(([name, tool]) => [
|
|
177
245
|
name,
|
|
178
246
|
{
|
|
247
|
+
__toolkitRef: true as const,
|
|
179
248
|
pluginName: "graphiti",
|
|
180
249
|
localName: name,
|
|
181
250
|
def: {
|
|
@@ -183,31 +252,130 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
|
183
252
|
description: tool.description,
|
|
184
253
|
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
185
254
|
},
|
|
255
|
+
annotations: {
|
|
256
|
+
effect: WRITE_TOOLS.has(name) ? ("write" as const) : ("read" as const),
|
|
257
|
+
requiresUserContext: true as const,
|
|
258
|
+
},
|
|
186
259
|
},
|
|
187
260
|
]),
|
|
188
261
|
);
|
|
189
262
|
}
|
|
190
263
|
|
|
191
|
-
async executeAgentTool(
|
|
264
|
+
async executeAgentTool(
|
|
265
|
+
name: string,
|
|
266
|
+
args: unknown,
|
|
267
|
+
signal?: AbortSignal,
|
|
268
|
+
context?: { resourceId?: string },
|
|
269
|
+
): Promise<unknown> {
|
|
192
270
|
const tool = this.mcpTools[name];
|
|
193
271
|
if (!tool?.execute) throw new Error(`Unknown Graphiti tool: ${name}`);
|
|
194
|
-
|
|
272
|
+
const userId = context?.resourceId ?? executionContextUserId();
|
|
273
|
+
return tool.execute(scopedArguments(name, args, userScope(userId)), {
|
|
274
|
+
abortSignal: signal,
|
|
275
|
+
} as never);
|
|
195
276
|
}
|
|
196
277
|
|
|
197
|
-
private
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
278
|
+
private async forwardMcp(request: express.Request, response: express.Response): Promise<void> {
|
|
279
|
+
if (!this.resolved) {
|
|
280
|
+
response.status(503).json({ error: "Graphiti is not ready" });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const userId = appkitIdentity.requestUserId(request) ?? executionContextUserId();
|
|
284
|
+
const server = this.mcpServer(userId);
|
|
285
|
+
await server.startHTTP({
|
|
286
|
+
url: new URL(request.originalUrl, "http://app.local"),
|
|
287
|
+
httpPath: MCP_PATH,
|
|
288
|
+
req: request,
|
|
289
|
+
res: response,
|
|
205
290
|
});
|
|
206
291
|
}
|
|
292
|
+
|
|
293
|
+
private mcpServer(userId: string): MCPServer {
|
|
294
|
+
const existing = this.mcpServers.get(userId);
|
|
295
|
+
if (existing) {
|
|
296
|
+
existing.lastUsed = Date.now();
|
|
297
|
+
return existing.server;
|
|
298
|
+
}
|
|
299
|
+
const scope = userScope(userId);
|
|
300
|
+
const tools = Object.fromEntries(
|
|
301
|
+
Object.entries(this.mcpTools).map(([name, tool]) => [
|
|
302
|
+
name,
|
|
303
|
+
createTool({
|
|
304
|
+
id: name,
|
|
305
|
+
description: tool.description ?? name,
|
|
306
|
+
...(tool.inputSchema ? { inputSchema: tool.inputSchema as never } : {}),
|
|
307
|
+
execute: (args: unknown, context: unknown) => {
|
|
308
|
+
if (!tool.execute) throw new Error(`Graphiti tool cannot execute: ${name}`);
|
|
309
|
+
return tool.execute(scopedArguments(name, args, scope), context as never);
|
|
310
|
+
},
|
|
311
|
+
}),
|
|
312
|
+
]),
|
|
313
|
+
);
|
|
314
|
+
const server = new MCPServer({
|
|
315
|
+
name: "Graphiti",
|
|
316
|
+
version: "1.0.0",
|
|
317
|
+
tools,
|
|
318
|
+
});
|
|
319
|
+
this.mcpServers.set(userId, { lastUsed: Date.now(), server });
|
|
320
|
+
return server;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private onSupervisorExit(error?: unknown): void {
|
|
324
|
+
if (this.stopping || !this.setupComplete) return;
|
|
325
|
+
this.logger.error("sidecar supervisor exited", { error });
|
|
326
|
+
process.kill(process.pid, "SIGTERM");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private async stopSidecars(): Promise<void> {
|
|
330
|
+
if (this.stopping) return;
|
|
331
|
+
this.stopping = true;
|
|
332
|
+
if (this.mcpServerSweep) clearInterval(this.mcpServerSweep);
|
|
333
|
+
this.mcpServerSweep = undefined;
|
|
334
|
+
await Promise.allSettled([...this.mcpServers.values()].map(({ server }) => server.close()));
|
|
335
|
+
this.mcpServers.clear();
|
|
336
|
+
await this.mcp?.disconnect();
|
|
337
|
+
this.mcp = undefined;
|
|
338
|
+
this.mcpTools = {};
|
|
339
|
+
for (const command of this.commands) command.kill("SIGTERM");
|
|
340
|
+
if (this.supervision) await this.supervision.result.catch(() => undefined);
|
|
341
|
+
this.commands = [];
|
|
342
|
+
this.supervision = undefined;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private closeIdleMcpServers(): void {
|
|
346
|
+
const cutoff = Date.now() - MCP_SERVER_IDLE_MS;
|
|
347
|
+
for (const [userId, entry] of this.mcpServers) {
|
|
348
|
+
if (entry.lastUsed >= cutoff) continue;
|
|
349
|
+
this.mcpServers.delete(userId);
|
|
350
|
+
void entry.server.close().catch((error) => {
|
|
351
|
+
this.logger.warn("idle MCP server close failed", { error });
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
207
355
|
}
|
|
208
356
|
|
|
209
357
|
export const graphiti = toPlugin(GraphitiPlugin);
|
|
210
358
|
|
|
359
|
+
function executionContextUserId(): string {
|
|
360
|
+
const context = dbxAppkit.tryGetExecutionContext();
|
|
361
|
+
if (!context) throw new Error("Graphiti memory requires an AppKit execution context");
|
|
362
|
+
return "userId" in context ? context.userId : context.serviceUserId;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function userScope(userId: string): string {
|
|
366
|
+
return `user_${createHash("sha256").update(userId).digest("base64url")}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function scopedArguments(name: string, args: unknown, scope: string): Record<string, unknown> {
|
|
370
|
+
if (!(name in SCOPED_TOOL_FIELDS)) throw new Error(`Graphiti tool is not user-scoped: ${name}`);
|
|
371
|
+
const scoped = object.isRecord(args) ? { ...args } : {};
|
|
372
|
+
for (const argument of UNSCOPED_ARGUMENTS) delete scoped[argument];
|
|
373
|
+
const field = SCOPED_TOOL_FIELDS[name as keyof typeof SCOPED_TOOL_FIELDS];
|
|
374
|
+
if (field === "group_id") scoped.group_id = scope;
|
|
375
|
+
if (field === "group_ids") scoped.group_ids = [scope];
|
|
376
|
+
return scoped;
|
|
377
|
+
}
|
|
378
|
+
|
|
211
379
|
async function waitForGraphiti(port: number): Promise<void> {
|
|
212
380
|
for await (const ready of asyncModule.poll(
|
|
213
381
|
async ({ signal }) => {
|
|
@@ -243,3 +411,25 @@ async function availablePort(): Promise<number> {
|
|
|
243
411
|
});
|
|
244
412
|
});
|
|
245
413
|
}
|
|
414
|
+
|
|
415
|
+
async function distinctPorts(
|
|
416
|
+
appPort: number,
|
|
417
|
+
graphitiPort: number,
|
|
418
|
+
litellmPort: number,
|
|
419
|
+
proxyPort: number,
|
|
420
|
+
): Promise<[number, number, number]> {
|
|
421
|
+
const ports = [appPort];
|
|
422
|
+
for (const configuredPort of [graphitiPort, litellmPort, proxyPort]) {
|
|
423
|
+
if (configuredPort && ports.includes(configuredPort)) {
|
|
424
|
+
throw new ConfigurationError("Graphiti sidecar ports must differ from DATABRICKS_APP_PORT");
|
|
425
|
+
}
|
|
426
|
+
let port = configuredPort || (await availablePort());
|
|
427
|
+
while (ports.includes(port)) port = await availablePort();
|
|
428
|
+
ports.push(port);
|
|
429
|
+
}
|
|
430
|
+
return ports.slice(1) as [number, number, number];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function commandLine(arguments_: string[]): string {
|
|
434
|
+
return arguments_.map((value) => `'${value.replaceAll("'", "'\\''")}'`).join(" ");
|
|
435
|
+
}
|