@smartergpt/lex-mcp 2.9.1 → 3.0.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 CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Thin MCP stdio wrapper for [@smartergpt/lex](https://github.com/Guffawaffle/lex) episodic memory.
4
4
 
5
- Lex owns all capabilities. This package owns delivery over [Model Context Protocol](https://modelcontextprotocol.io/).
5
+ Lex owns all capabilities and authority decisions. This package owns delivery
6
+ over [Model Context Protocol](https://modelcontextprotocol.io/).
6
7
 
7
8
  ## Release Contract
8
9
 
@@ -12,12 +13,51 @@ exact version, reports that version through MCP `serverInfo`, and supports the
12
13
  same Node range as Lex (`>=20 <25`). Publish the matching Lex release before
13
14
  publishing this wrapper.
14
15
 
15
- ## Quick Start
16
+ Prepublication CI applies the staged wrapper version only to its disposable Lex
17
+ checkout, then builds, installs, and packs that local source. After Lex is
18
+ published, refresh this repository's registry lock with
19
+ `npm install --package-lock-only --ignore-scripts`, verify no `file:` dependency
20
+ was introduced, commit the resulting integrity, and only then tag or publish
21
+ Lex MCP. The release workflow enforces that post-publish integrity gate.
22
+
23
+ ## Local Compatibility Launch
16
24
 
17
25
  ```bash
18
26
  npx @smartergpt/lex-mcp
19
27
  ```
20
28
 
29
+ The executable is the backwards-compatible, local single-workspace launch. It
30
+ uses the current directory or `LEX_WORKSPACE_ROOT` for project discovery and
31
+ delegates local store configuration to Lex. Those values configure the local
32
+ process; they do not establish canonical authority, grants, or a tenant scope.
33
+
34
+ ## Trusted Lex 3 Host
35
+
36
+ Multi-tenant and cross-workspace hosts compose authority explicitly with Lex,
37
+ then pass Lex's MCP options through this package unchanged:
38
+
39
+ ```js
40
+ import { startLexMcpStdio } from "@smartergpt/lex-mcp";
41
+ import { createPostgresTrustedRuntimeHost } from "@smartergpt/lex/runtime-scope";
42
+
43
+ const host = createPostgresTrustedRuntimeHost({
44
+ authorityPool, // read-only runtime authority connection
45
+ selection, // authenticated tenant/workspace selection owned by this host
46
+ frameStoreBinder, // scope-bound PostgreSQL frame store binder
47
+ process: capturedProcessEvidence,
48
+ runtimeId,
49
+ traceId,
50
+ emitDiagnostics,
51
+ });
52
+
53
+ const transport = startLexMcpStdio({ serverOptions: host.mcp });
54
+ ```
55
+
56
+ The host must supply its pools, authenticated selection, process evidence, and
57
+ IDs. Neither Lex nor this wrapper reconstructs trusted authority from
58
+ environment variables. See Lex's runtime-scope documentation for the complete
59
+ host composition and PostgreSQL RLS contracts.
60
+
21
61
  ## Configuration
22
62
 
23
63
  ### VS Code / Copilot
@@ -59,18 +99,37 @@ Add to `claude_desktop_config.json`:
59
99
  echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npx @smartergpt/lex-mcp
60
100
  ```
61
101
 
62
- ## Environment Variables
102
+ ## Compatibility-Launcher Environment Variables
63
103
 
64
- | Variable | Description | Default |
65
- | -------------------- | -------------------------------------------- | --------------------------- |
66
- | `LEX_WORKSPACE_ROOT` | Project root directory (compat env var name) | Current directory |
67
- | `LEX_DB_PATH` | SQLite database path (canonical) | `.smartergpt/lex/memory.db` |
68
- | `LEX_MEMORY_DB` | Alias for `LEX_DB_PATH` (compat only) | — |
69
- | `LEX_DEBUG` | Enable debug logging to stderr | Off |
104
+ | Variable | Description | Default |
105
+ | ----------------------- | ------------------------------------------------- | --------------------------- |
106
+ | `LEX_WORKSPACE_ROOT` | Local project root | Current directory |
107
+ | `LEX_STORE` | Compatibility frame backend (`sqlite`/`postgres`) | `sqlite` |
108
+ | `LEX_DATABASE_URL` | Compatibility PostgreSQL connection URL | — |
109
+ | `LEX_POSTGRES_PASSWORD` | Password for a credential-free compatibility URL | |
110
+ | `LEX_POSTGRES_POOL_MAX` | Compatibility PostgreSQL pool size | `10` |
111
+ | `LEX_DB_PATH` | SQLite database path; ignored by PostgreSQL | `.smartergpt/lex/memory.db` |
112
+ | `LEX_MEMORY_DB` | Alias for `LEX_DB_PATH` (compat only) | — |
113
+ | `LEX_DEBUG` | Enable debug logging to stderr | Off |
70
114
 
71
115
  When both `LEX_DB_PATH` and `LEX_MEMORY_DB` are set, `LEX_DB_PATH` wins.
72
116
 
73
- For multi-root workspaces, set the same absolute `LEX_DB_PATH` for direct Lex, this MCP wrapper, and AXF-routed Lex. The wrapper delegates `.lex.config.json`, environment, and default-store resolution to Lex core, so installed launches now honor caller-project config files. Use `system_introspect` to compare canonical store paths and `path-v1` identities across launch paths.
117
+ For multi-root compatibility launches using SQLite, set the same absolute
118
+ `LEX_DB_PATH` for direct Lex, this MCP wrapper, and AXF-routed Lex. The wrapper
119
+ delegates `.lex.config.json`, environment, and local store resolution to Lex
120
+ core, so installed launches honor caller-project config files. Shared trusted
121
+ PostgreSQL deployments must instead inject canonical authority and a scoped
122
+ store binder as shown above; environment configuration alone is never a trusted
123
+ multi-tenant bootstrap.
124
+
125
+ ## Agent Output
126
+
127
+ Agents can request `format: "compact"` on supported frame and introspection
128
+ tools to avoid redundant presentation metadata. Runtime-scope diagnostics are
129
+ absent by default and appear only when a caller explicitly requests
130
+ `diagnostics: "summary"` or `"full"` and has the required authority. Output
131
+ format and diagnostics affect presentation only; they never change scope or
132
+ authorization outcomes.
74
133
 
75
134
  ## Tools
76
135
 
package/index.mjs CHANGED
@@ -8,111 +8,51 @@
8
8
  * Usage:
9
9
  * npx @smartergpt/lex-mcp
10
10
  *
11
- * Environment Variables:
12
- * LEX_WORKSPACE_ROOT - Project root directory (compat env var name, default: cwd)
11
+ * Compatibility-launch environment variables:
12
+ * LEX_WORKSPACE_ROOT - Project root directory (default: cwd)
13
13
  * LEX_DB_PATH - SQLite database path (default: .smartergpt/lex/memory.db)
14
14
  * LEX_MEMORY_DB - Alias for LEX_DB_PATH (for backwards compatibility)
15
15
  * LEX_DEBUG - Enable debug logging to stderr
16
16
  */
17
17
 
18
- import { MCPServer } from "@smartergpt/lex/mcp-server";
18
+ import { startLexMcpStdio } from "./stdio.mjs";
19
19
 
20
20
  // Project root defaults to current working directory.
21
21
  const projectRoot = process.env.LEX_WORKSPACE_ROOT || process.cwd();
22
22
 
23
- // Set LEX_WORKSPACE_ROOT for modules that need it
24
- if (!process.env.LEX_WORKSPACE_ROOT) {
25
- process.env.LEX_WORKSPACE_ROOT = projectRoot;
26
- }
27
-
28
- // Initialize MCP server
29
- let mcpServer;
23
+ // This executable preserves the local, single-workspace compatibility launch.
24
+ // Environment and cwd select local configuration; they never establish trusted
25
+ // Lex 3 authority. Trusted hosts import startLexMcpStdio and inject host.mcp.
26
+ let transport;
30
27
  try {
31
- // Lex core owns env, .lex.config.json, and default-path precedence. Passing
32
- // only the caller root prevents the delivery wrapper from masking config.
33
- mcpServer = new MCPServer({ repoRoot: projectRoot });
28
+ transport = startLexMcpStdio({
29
+ serverOptions: { repoRoot: projectRoot },
30
+ debug: Boolean(process.env.LEX_DEBUG),
31
+ });
34
32
  if (process.env.LEX_DEBUG) {
35
33
  console.error(`[LEX-MCP] Project root: ${projectRoot}`);
34
+ console.error("[LEX-MCP] Ready (stdio compatibility mode)");
36
35
  }
37
36
  } catch (error) {
38
37
  console.error(`[LEX-MCP] Failed to initialize: ${error.message}`);
39
38
  process.exit(1);
40
39
  }
41
40
 
42
- if (process.env.LEX_DEBUG) {
43
- console.error(`[LEX-MCP] Ready (stdio mode)`);
44
- }
45
-
46
- // MCP stdio protocol handler (JSON-RPC 2.0 over newline-delimited JSON)
47
- process.stdin.setEncoding("utf8");
48
- let buffer = "";
49
-
50
- process.stdin.on("data", async (chunk) => {
51
- buffer += chunk;
52
- const lines = buffer.split("\n");
53
- buffer = lines.pop() || "";
54
-
55
- for (const line of lines) {
56
- if (!line.trim()) continue;
57
-
58
- let request;
59
- try {
60
- request = JSON.parse(line);
61
-
62
- // MCP notifications have no `id` — silently ignore them per spec
63
- if (request.id === undefined || request.id === null) {
64
- if (process.env.LEX_DEBUG) {
65
- console.error(`[LEX-MCP] Notification: ${request.method}`);
66
- }
67
- continue;
68
- }
69
-
70
- const response = await mcpServer.handleRequest(request);
71
-
72
- // MCP protocol response format
73
- if (response.error) {
74
- console.log(
75
- JSON.stringify({
76
- jsonrpc: "2.0",
77
- id: request.id,
78
- error: response.error,
79
- }),
80
- );
81
- } else {
82
- console.log(
83
- JSON.stringify({
84
- jsonrpc: "2.0",
85
- id: request.id,
86
- result: response,
87
- }),
88
- );
89
- }
90
- } catch (error) {
91
- console.log(
92
- JSON.stringify({
93
- jsonrpc: "2.0",
94
- id: request?.id || null,
95
- error: {
96
- message: error.message,
97
- code: error.code || "PARSE_ERROR",
98
- },
99
- }),
100
- );
101
- }
102
- }
103
- });
104
-
105
41
  // Graceful shutdown
106
- process.on("SIGINT", () => {
107
- if (mcpServer) mcpServer.close();
108
- process.exit(0);
109
- });
110
-
111
- process.on("SIGTERM", () => {
112
- if (mcpServer) mcpServer.close();
113
- process.exit(0);
114
- });
115
-
116
- if (process.env.LEX_DEBUG) {
117
- console.error("[LEX-MCP] Ready (stdio mode)");
42
+ let shuttingDown = false;
43
+ async function shutdown() {
44
+ if (shuttingDown) return;
45
+ shuttingDown = true;
46
+ try {
47
+ await transport?.close();
48
+ process.exit(0);
49
+ } catch (error) {
50
+ console.error(
51
+ `[LEX-MCP] Failed to shut down: ${error instanceof Error ? error.message : String(error)}`,
52
+ );
53
+ process.exit(1);
54
+ }
118
55
  }
56
+
57
+ process.on("SIGINT", () => void shutdown());
58
+ process.on("SIGTERM", () => void shutdown());
package/package.json CHANGED
@@ -1,16 +1,32 @@
1
1
  {
2
2
  "name": "@smartergpt/lex-mcp",
3
- "version": "2.9.1",
3
+ "version": "3.0.0",
4
4
  "description": "MCP server for Lex episodic memory - stdio transport wrapper",
5
5
  "mcpName": "dev.smartergpt/lex",
6
6
  "type": "module",
7
+ "main": "./stdio.mjs",
8
+ "types": "./stdio.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./stdio.d.ts",
12
+ "import": "./stdio.mjs"
13
+ },
14
+ "./stdio": {
15
+ "types": "./stdio.d.ts",
16
+ "import": "./stdio.mjs"
17
+ }
18
+ },
7
19
  "bin": "./index.mjs",
8
20
  "files": [
9
21
  "index.mjs",
22
+ "stdio.mjs",
23
+ "stdio.d.ts",
10
24
  "README.md"
11
25
  ],
12
26
  "scripts": {
13
- "test": "node test-contract.mjs && node test-mcp-server.mjs --local --clean",
27
+ "test": "npm run test:types && node test-stdio.mjs && node test-contract.mjs && node test-mcp-server.mjs --local --clean",
28
+ "test:types": "tsc --noEmit --skipLibCheck --module NodeNext --moduleResolution NodeNext --target ES2022 stdio.d.ts test-types.mts",
29
+ "test:stdio": "node test-stdio.mjs",
14
30
  "test:contract": "node test-contract.mjs",
15
31
  "test:contract:verbose": "node test-contract.mjs --verbose",
16
32
  "test:mcp": "node test-mcp-server.mjs --local --clean",
@@ -22,7 +38,7 @@
22
38
  "link:local": "rm -rf node_modules/@smartergpt/lex && mkdir -p node_modules/@smartergpt && ln -s /srv/lex-mcp/lex node_modules/@smartergpt/lex"
23
39
  },
24
40
  "dependencies": {
25
- "@smartergpt/lex": "2.9.1"
41
+ "@smartergpt/lex": "3.0.0"
26
42
  },
27
43
  "publishConfig": {
28
44
  "access": "public",
package/stdio.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ import type { MCPServer, MCPServerOptions } from "@smartergpt/lex/mcp-server";
3
+
4
+ export type LexMcpRequestServer = Pick<MCPServer, "handleRequest" | "close">;
5
+
6
+ export interface LexMcpStdioOptions {
7
+ /** Explicit Lex configuration, including a trusted host's unmodified `host.mcp`. */
8
+ serverOptions: MCPServerOptions;
9
+ input?: Readable;
10
+ output?: Writable;
11
+ errorOutput?: Writable;
12
+ debug?: boolean;
13
+ /** Optional embedding/test seam. Receives `serverOptions` by identity. */
14
+ serverFactory?: (serverOptions: MCPServerOptions) => LexMcpRequestServer;
15
+ }
16
+
17
+ export interface LexMcpStdioTransport {
18
+ readonly server: LexMcpRequestServer;
19
+ /** Resolve once received records are handled; after EOF, includes server close. */
20
+ idle(): Promise<void>;
21
+ /** Stop input, flush a trailing record, drain requests, and close Lex once. */
22
+ close(): Promise<void>;
23
+ }
24
+
25
+ export function startLexMcpStdio(
26
+ options: LexMcpStdioOptions,
27
+ ): LexMcpStdioTransport;
package/stdio.mjs ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Newline-delimited JSON-RPC transport for Lex's MCP server.
3
+ *
4
+ * This module deliberately owns transport only. Canonical authority, workspace
5
+ * selection, and scope-bound stores must be composed by the trusted host and
6
+ * passed through `serverOptions`; they are never reconstructed from ambient
7
+ * process state here.
8
+ */
9
+
10
+ import { MCPServer } from "@smartergpt/lex/mcp-server";
11
+
12
+ /** @typedef {import("@smartergpt/lex/mcp-server").MCPServerOptions} MCPServerOptions */
13
+
14
+ const JSON_RPC_PARSE_ERROR = -32700;
15
+ const JSON_RPC_INVALID_REQUEST = -32600;
16
+ const JSON_RPC_APPLICATION_ERROR = -32000;
17
+
18
+ function errorMessage(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+
22
+ function isRecord(value) {
23
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+
26
+ function applicationError(error) {
27
+ const record = isRecord(error) ? error : {};
28
+ const sourceCode = record.code;
29
+ const code = Number.isInteger(sourceCode)
30
+ ? sourceCode
31
+ : JSON_RPC_APPLICATION_ERROR;
32
+ const message =
33
+ typeof record.message === "string" ? record.message : errorMessage(error);
34
+
35
+ const data = isRecord(record.data)
36
+ ? { ...record.data }
37
+ : record.data === undefined
38
+ ? {}
39
+ : { details: record.data };
40
+ if (typeof sourceCode === "string") {
41
+ data.lexCode = sourceCode;
42
+ }
43
+ for (const field of ["context", "nextActions", "metadata"]) {
44
+ if (record[field] !== undefined) data[field] = record[field];
45
+ }
46
+
47
+ return {
48
+ code,
49
+ message,
50
+ ...(Object.keys(data).length === 0 ? {} : { data }),
51
+ };
52
+ }
53
+
54
+ function isValidRequestEnvelope(request) {
55
+ return (
56
+ isRecord(request) &&
57
+ request.jsonrpc === "2.0" &&
58
+ typeof request.method === "string" &&
59
+ request.method.length > 0
60
+ );
61
+ }
62
+
63
+ function isValidRequestId(id) {
64
+ return (
65
+ typeof id === "string" || (typeof id === "number" && Number.isFinite(id))
66
+ );
67
+ }
68
+
69
+ /**
70
+ * Start a Lex MCP server on newline-delimited JSON stdio.
71
+ *
72
+ * Trusted Lex 3 hosts pass the unmodified `host.mcp` object returned by
73
+ * `createPostgresTrustedRuntimeHost`. The optional `serverFactory` is intended
74
+ * for embedding and transport tests; it receives the same options object by
75
+ * identity.
76
+ *
77
+ * @param {object} options
78
+ * @param {MCPServerOptions} options.serverOptions
79
+ * @param {NodeJS.ReadableStream} [options.input]
80
+ * @param {NodeJS.WritableStream} [options.output]
81
+ * @param {NodeJS.WritableStream} [options.errorOutput]
82
+ * @param {boolean} [options.debug]
83
+ * @param {(serverOptions: MCPServerOptions) => Pick<MCPServer, "handleRequest" | "close">} [options.serverFactory]
84
+ */
85
+ export function startLexMcpStdio({
86
+ serverOptions,
87
+ input = process.stdin,
88
+ output = process.stdout,
89
+ errorOutput = process.stderr,
90
+ debug = false,
91
+ serverFactory = (options) => new MCPServer(options),
92
+ }) {
93
+ if (!serverOptions || typeof serverOptions !== "object") {
94
+ throw new TypeError(
95
+ "serverOptions must be an explicit MCPServerOptions object",
96
+ );
97
+ }
98
+
99
+ const mcpServer = serverFactory(serverOptions);
100
+ if (
101
+ !mcpServer ||
102
+ typeof mcpServer.handleRequest !== "function" ||
103
+ typeof mcpServer.close !== "function"
104
+ ) {
105
+ throw new TypeError(
106
+ "serverFactory must return an MCPServer-compatible server",
107
+ );
108
+ }
109
+
110
+ input.setEncoding?.("utf8");
111
+ let buffer = "";
112
+ let work = Promise.resolve();
113
+ let closePromise;
114
+
115
+ const writeResponse = (response) => {
116
+ output.write(`${JSON.stringify(response)}\n`);
117
+ };
118
+
119
+ const handleLine = async (line) => {
120
+ if (!line.trim()) return;
121
+
122
+ let request;
123
+ try {
124
+ request = JSON.parse(line);
125
+ } catch {
126
+ writeResponse({
127
+ jsonrpc: "2.0",
128
+ id: null,
129
+ error: { code: JSON_RPC_PARSE_ERROR, message: "Parse error" },
130
+ });
131
+ return;
132
+ }
133
+
134
+ if (!isValidRequestEnvelope(request)) {
135
+ writeResponse({
136
+ jsonrpc: "2.0",
137
+ id: null,
138
+ error: { code: JSON_RPC_INVALID_REQUEST, message: "Invalid Request" },
139
+ });
140
+ return;
141
+ }
142
+
143
+ // Only an absent id denotes a notification. JSON-RPC permits clients to
144
+ // send null, but Lex 3 treats it as invalid so request/response ownership
145
+ // remains unambiguous.
146
+ if (!Object.hasOwn(request, "id")) {
147
+ if (debug) {
148
+ errorOutput.write(`[LEX-MCP] Notification: ${request.method}\n`);
149
+ }
150
+ try {
151
+ // Notifications participate in the same serialized dispatch stream as
152
+ // requests, but JSON-RPC forbids returning either a result or an error
153
+ // response for them.
154
+ await mcpServer.handleRequest(request);
155
+ } catch (error) {
156
+ errorOutput.write(
157
+ `[LEX-MCP] Notification failed (${request.method}): ${errorMessage(error)}\n`,
158
+ );
159
+ }
160
+ return;
161
+ }
162
+
163
+ if (!isValidRequestId(request.id)) {
164
+ writeResponse({
165
+ jsonrpc: "2.0",
166
+ id: null,
167
+ error: { code: JSON_RPC_INVALID_REQUEST, message: "Invalid Request" },
168
+ });
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const response = await mcpServer.handleRequest(request);
174
+ if (!isRecord(response)) {
175
+ throw new TypeError("Lex returned an invalid MCP response");
176
+ }
177
+ writeResponse(
178
+ response.error
179
+ ? {
180
+ jsonrpc: "2.0",
181
+ id: request.id,
182
+ error: applicationError(response.error),
183
+ }
184
+ : { jsonrpc: "2.0", id: request.id, result: response },
185
+ );
186
+ } catch (error) {
187
+ writeResponse({
188
+ jsonrpc: "2.0",
189
+ id: request.id,
190
+ error: applicationError(error),
191
+ });
192
+ }
193
+ };
194
+
195
+ const enqueueLine = (line) => {
196
+ work = work.then(() => handleLine(line));
197
+ };
198
+
199
+ const flushBuffer = () => {
200
+ if (!buffer.trim()) {
201
+ buffer = "";
202
+ return;
203
+ }
204
+ const line = buffer;
205
+ buffer = "";
206
+ enqueueLine(line);
207
+ };
208
+
209
+ const onData = (chunk) => {
210
+ buffer += String(chunk);
211
+ const lines = buffer.split("\n");
212
+ buffer = lines.pop() || "";
213
+
214
+ // Serialize requests in wire order. This avoids interleaving responses or
215
+ // request-local scope stores when multiple lines arrive in one chunk.
216
+ for (const line of lines) {
217
+ enqueueLine(line);
218
+ }
219
+ };
220
+
221
+ const close = () => {
222
+ if (closePromise) return closePromise;
223
+
224
+ input.off?.("data", onData);
225
+ input.off?.("end", onEnd);
226
+ flushBuffer();
227
+ closePromise = (async () => {
228
+ try {
229
+ await work;
230
+ } finally {
231
+ await mcpServer.close();
232
+ }
233
+ })();
234
+ return closePromise;
235
+ };
236
+
237
+ // EOF is the transport lifecycle boundary: accept a final record without a
238
+ // newline, drain every queued request, then close the Lex server exactly once.
239
+ const onEnd = () => {
240
+ void close().catch((error) => {
241
+ errorOutput.write(`[LEX-MCP] Failed to close: ${errorMessage(error)}\n`);
242
+ });
243
+ };
244
+
245
+ input.on("data", onData);
246
+ input.on("end", onEnd);
247
+
248
+ return Object.freeze({
249
+ server: mcpServer,
250
+ idle: () => closePromise ?? work,
251
+ close,
252
+ });
253
+ }