@smartergpt/lex-mcp 2.10.0 → 3.0.1
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 +66 -11
- package/index.mjs +28 -88
- package/package.json +19 -3
- package/stdio.d.ts +27 -0
- package/stdio.mjs +253 -0
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
|
|
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
|
-
|
|
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,22 +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
|
|
65
|
-
| ----------------------- |
|
|
66
|
-
| `LEX_WORKSPACE_ROOT` |
|
|
67
|
-
| `LEX_STORE` |
|
|
68
|
-
| `LEX_DATABASE_URL` | PostgreSQL URL
|
|
69
|
-
| `LEX_POSTGRES_PASSWORD` |
|
|
70
|
-
| `LEX_POSTGRES_POOL_MAX` |
|
|
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` |
|
|
71
111
|
| `LEX_DB_PATH` | SQLite database path; ignored by PostgreSQL | `.smartergpt/lex/memory.db` |
|
|
72
112
|
| `LEX_MEMORY_DB` | Alias for `LEX_DB_PATH` (compat only) | — |
|
|
73
113
|
| `LEX_DEBUG` | Enable debug logging to stderr | Off |
|
|
74
114
|
|
|
75
115
|
When both `LEX_DB_PATH` and `LEX_MEMORY_DB` are set, `LEX_DB_PATH` wins.
|
|
76
116
|
|
|
77
|
-
For multi-root
|
|
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.
|
|
78
133
|
|
|
79
134
|
## Tools
|
|
80
135
|
|
package/index.mjs
CHANGED
|
@@ -8,111 +8,51 @@
|
|
|
8
8
|
* Usage:
|
|
9
9
|
* npx @smartergpt/lex-mcp
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* LEX_WORKSPACE_ROOT - Project root directory (
|
|
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 {
|
|
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
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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": "
|
|
3
|
+
"version": "3.0.1",
|
|
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": "
|
|
41
|
+
"@smartergpt/lex": "3.0.1"
|
|
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
|
+
}
|