@dbx-tools/appkit-graphiti 0.6.113 → 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 +50 -33
- package/lib/src/config.d.ts +8 -15
- package/lib/src/config.js +13 -27
- package/lib/src/plugin.d.ts +21 -8
- package/lib/src/plugin.js +250 -87
- package/package.json +7 -5
- package/src/config.ts +16 -46
- package/src/plugin.ts +288 -89
package/src/config.ts
CHANGED
|
@@ -1,26 +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;
|
|
13
|
+
proxyPort?: number;
|
|
15
14
|
python?: string;
|
|
16
15
|
journalNamespace?: string;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
|
|
20
|
-
publicPort: number;
|
|
21
|
-
appPort: number;
|
|
22
19
|
graphitiPort: number;
|
|
23
|
-
|
|
20
|
+
litellmPort: number;
|
|
21
|
+
proxyPort: number;
|
|
24
22
|
python: string;
|
|
25
23
|
journalNamespace: string;
|
|
26
24
|
}
|
|
@@ -28,10 +26,9 @@ export interface ResolvedGraphitiPluginConfig extends GraphitiPluginConfig {
|
|
|
28
26
|
export const GRAPHITI_CONFIG_SCHEMA = {
|
|
29
27
|
type: "object",
|
|
30
28
|
properties: {
|
|
31
|
-
publicPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
32
|
-
appPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
33
29
|
graphitiPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
34
|
-
|
|
30
|
+
litellmPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
31
|
+
proxyPort: { type: "integer", minimum: 1, maximum: 65535 },
|
|
35
32
|
python: { type: "string" },
|
|
36
33
|
journalNamespace: { type: "string" },
|
|
37
34
|
},
|
|
@@ -42,58 +39,31 @@ export const GRAPHITI_CONFIG_SCHEMA = {
|
|
|
42
39
|
export function resolveGraphitiConfig(
|
|
43
40
|
config: GraphitiPluginConfig = {},
|
|
44
41
|
): ResolvedGraphitiPluginConfig {
|
|
45
|
-
const publicPort = coreConfig.port(
|
|
46
|
-
config.publicPort,
|
|
47
|
-
"DATABRICKS_APP_PORT",
|
|
48
|
-
8000,
|
|
49
|
-
coreConfig.ENV_ONLY,
|
|
50
|
-
);
|
|
51
|
-
const appPort = coreConfig.port(
|
|
52
|
-
config.appPort,
|
|
53
|
-
"GRAPHITI_APP_PORT",
|
|
54
|
-
publicPort + 1,
|
|
55
|
-
coreConfig.ENV_ONLY,
|
|
56
|
-
);
|
|
57
42
|
const graphitiPort = coreConfig.port(
|
|
58
43
|
config.graphitiPort,
|
|
59
44
|
"GRAPHITI_PORT",
|
|
60
45
|
0,
|
|
61
46
|
coreConfig.ENV_ONLY,
|
|
62
47
|
);
|
|
63
|
-
|
|
64
|
-
|
|
48
|
+
const litellmPort = coreConfig.port(config.litellmPort, "LITELLM_PORT", 0, coreConfig.ENV_ONLY);
|
|
49
|
+
const proxyPort = coreConfig.port(config.proxyPort, "PROXY_PORT", 0, coreConfig.ENV_ONLY);
|
|
50
|
+
const configuredPorts = [graphitiPort, litellmPort, proxyPort].filter(Boolean);
|
|
51
|
+
if (new Set(configuredPorts).size !== configuredPorts.length) {
|
|
52
|
+
throw new ConfigurationError("Graphiti sidecar ports must be distinct");
|
|
65
53
|
}
|
|
66
|
-
const routePrefix = normalizeRoutePrefix(
|
|
67
|
-
config.routePrefix ??
|
|
68
|
-
coreConfig.text("GRAPHITI_ROUTE_PREFIX", coreConfig.ENV_ONLY) ??
|
|
69
|
-
"/graphiti",
|
|
70
|
-
);
|
|
71
54
|
const python =
|
|
72
55
|
config.python ?? coreConfig.text("PYTHON", coreConfig.ENV_ONLY)?.trim() ?? "python3";
|
|
73
56
|
const journalNamespace =
|
|
74
57
|
config.journalNamespace ??
|
|
75
58
|
coreConfig.text("JOURNAL_NAMESPACE", coreConfig.ENV_ONLY)?.trim() ??
|
|
76
59
|
process.env.DATABRICKS_APP_NAME?.trim() ??
|
|
60
|
+
coreProject.name() ??
|
|
77
61
|
"default";
|
|
78
62
|
return {
|
|
79
|
-
publicPort,
|
|
80
|
-
appPort,
|
|
81
63
|
graphitiPort,
|
|
82
|
-
|
|
64
|
+
litellmPort,
|
|
65
|
+
proxyPort,
|
|
83
66
|
python,
|
|
84
67
|
journalNamespace,
|
|
85
68
|
};
|
|
86
69
|
}
|
|
87
|
-
|
|
88
|
-
/** Return the internal port an AppKit `server()` plugin should bind. */
|
|
89
|
-
export function appPort(config: GraphitiPluginConfig = {}): number {
|
|
90
|
-
return resolveGraphitiConfig(config).appPort;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function normalizeRoutePrefix(value: string): string {
|
|
94
|
-
const normalized = `/${value.trim().replace(/^\/+|\/+$/g, "")}`;
|
|
95
|
-
if (normalized === "/") {
|
|
96
|
-
throw new ConfigurationError("Graphiti routePrefix must contain a path segment");
|
|
97
|
-
}
|
|
98
|
-
return normalized;
|
|
99
|
-
}
|
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,108 +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);
|
|
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
|
+
);
|
|
69
120
|
this.resolved = {
|
|
70
121
|
...configured,
|
|
71
|
-
graphitiPort
|
|
122
|
+
graphitiPort,
|
|
123
|
+
litellmPort,
|
|
124
|
+
proxyPort,
|
|
72
125
|
};
|
|
73
|
-
|
|
74
|
-
throw new Error(
|
|
75
|
-
"Graphiti requires Lakebase; register lakebase() before graphiti() and bind its Postgres resource",
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
|
79
|
-
process.once(signal, () => this.stopGraphiti());
|
|
80
|
-
}
|
|
81
|
-
const supervisor = appkitInterceptor.createInterceptorContext({});
|
|
82
|
-
supervisor.context.onTeardown(() => this.stopGraphiti());
|
|
83
|
-
const graphiti = exec.spawn(this.resolved.python, ["-m", "dbx_tools.graphiti", "start"], {
|
|
84
|
-
env: {
|
|
85
|
-
...process.env,
|
|
86
|
-
GRAPHITI_HOST: "127.0.0.1",
|
|
87
|
-
GRAPHITI_PORT: String(this.resolved.graphitiPort),
|
|
88
|
-
JOURNAL_NAMESPACE: this.resolved.journalNamespace,
|
|
89
|
-
},
|
|
90
|
-
stdin: "ignore",
|
|
91
|
-
stdout: "inherit",
|
|
92
|
-
stderr: "inherit",
|
|
93
|
-
});
|
|
94
|
-
const caddy = exec.spawn(
|
|
95
|
-
this.resolved.python,
|
|
126
|
+
this.supervision = concurrently(
|
|
96
127
|
[
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
},
|
|
107
154
|
],
|
|
108
155
|
{
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
156
|
+
killOthersOn: ["failure", "success"],
|
|
157
|
+
killSignal: "SIGTERM",
|
|
158
|
+
killTimeout: 12_000,
|
|
159
|
+
prefix: "name",
|
|
160
|
+
prefixColors: false,
|
|
113
161
|
},
|
|
114
162
|
);
|
|
115
|
-
this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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;
|
|
124
174
|
},
|
|
125
|
-
});
|
|
126
|
-
const discovered = await this.mcp.listTools();
|
|
127
|
-
this.mcpTools = Object.fromEntries(
|
|
128
|
-
Object.entries(discovered).map(([name, tool]) => [
|
|
129
|
-
name.replace(/^graphiti_/, ""),
|
|
130
|
-
tool as Tool,
|
|
131
|
-
]),
|
|
132
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();
|
|
133
209
|
this.logger.info("ready", {
|
|
134
|
-
appPort: this.resolved.appPort,
|
|
135
210
|
graphitiPort: this.resolved.graphitiPort,
|
|
136
|
-
|
|
137
|
-
|
|
211
|
+
litellmPort: this.resolved.litellmPort,
|
|
212
|
+
proxyPort: this.resolved.proxyPort,
|
|
213
|
+
mcpPath: MCP_PATH,
|
|
138
214
|
tools: Object.keys(this.mcpTools),
|
|
139
215
|
});
|
|
140
216
|
}
|
|
141
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
|
+
|
|
142
229
|
override abortActiveOperations(): void {
|
|
143
230
|
super.abortActiveOperations();
|
|
144
|
-
this.stopGraphiti();
|
|
145
231
|
void this.mcp?.disconnect();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
this.children = [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async shutdown(): Promise<void> {
|
|
235
|
+
await this.stopSidecars();
|
|
152
236
|
}
|
|
153
237
|
|
|
154
238
|
override exports() {
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
appPort: resolved.appPort,
|
|
158
|
-
graphitiPort: resolved.graphitiPort,
|
|
159
|
-
mcpPath: `${resolved.routePrefix}/mcp/`,
|
|
160
|
-
publicPort: resolved.publicPort,
|
|
161
|
-
routePrefix: resolved.routePrefix,
|
|
162
|
-
};
|
|
239
|
+
return { mcpPath: MCP_PATH };
|
|
163
240
|
}
|
|
164
241
|
|
|
165
242
|
toolkit(): Record<string, ToolkitEntry> {
|
|
@@ -167,6 +244,7 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
|
167
244
|
Object.entries(this.mcpTools).map(([name, tool]) => [
|
|
168
245
|
name,
|
|
169
246
|
{
|
|
247
|
+
__toolkitRef: true as const,
|
|
170
248
|
pluginName: "graphiti",
|
|
171
249
|
localName: name,
|
|
172
250
|
def: {
|
|
@@ -174,31 +252,130 @@ export class GraphitiPlugin extends Plugin<GraphitiPluginConfig> {
|
|
|
174
252
|
description: tool.description,
|
|
175
253
|
parameters: tool.inputSchema ?? { type: "object", properties: {} },
|
|
176
254
|
},
|
|
255
|
+
annotations: {
|
|
256
|
+
effect: WRITE_TOOLS.has(name) ? ("write" as const) : ("read" as const),
|
|
257
|
+
requiresUserContext: true as const,
|
|
258
|
+
},
|
|
177
259
|
},
|
|
178
260
|
]),
|
|
179
261
|
);
|
|
180
262
|
}
|
|
181
263
|
|
|
182
|
-
async executeAgentTool(
|
|
264
|
+
async executeAgentTool(
|
|
265
|
+
name: string,
|
|
266
|
+
args: unknown,
|
|
267
|
+
signal?: AbortSignal,
|
|
268
|
+
context?: { resourceId?: string },
|
|
269
|
+
): Promise<unknown> {
|
|
183
270
|
const tool = this.mcpTools[name];
|
|
184
271
|
if (!tool?.execute) throw new Error(`Unknown Graphiti tool: ${name}`);
|
|
185
|
-
|
|
272
|
+
const userId = context?.resourceId ?? executionContextUserId();
|
|
273
|
+
return tool.execute(scopedArguments(name, args, userScope(userId)), {
|
|
274
|
+
abortSignal: signal,
|
|
275
|
+
} as never);
|
|
186
276
|
}
|
|
187
277
|
|
|
188
|
-
private
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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,
|
|
196
290
|
});
|
|
197
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
|
+
}
|
|
198
355
|
}
|
|
199
356
|
|
|
200
357
|
export const graphiti = toPlugin(GraphitiPlugin);
|
|
201
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
|
+
|
|
202
379
|
async function waitForGraphiti(port: number): Promise<void> {
|
|
203
380
|
for await (const ready of asyncModule.poll(
|
|
204
381
|
async ({ signal }) => {
|
|
@@ -234,3 +411,25 @@ async function availablePort(): Promise<number> {
|
|
|
234
411
|
});
|
|
235
412
|
});
|
|
236
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
|
+
}
|