@wrongstack/sage-mcp 0.296.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ECOSTACK TECHNOLOGY OÜ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # @wrongstack/sage-mcp
2
+
3
+ WrongStack **SAGE Memory** exposed as an [MCP](https://modelcontextprotocol.io/) server. Use it from any MCP-compatible client (Claude Desktop, another agent, an IDE) to read, write, and curate your project's structured long-term memory.
4
+
5
+ Two operating modes:
6
+
7
+ | Mode | Entry point | What it does |
8
+ |------|-------------|--------------|
9
+ | **Standalone** | the `wstack-sage-mcp` binary this package ships | Boots *only* SAGE Memory — no `@wrongstack/cli` needed — and serves it over MCP. Connects to the existing SAGE IPC project server (Unix socket / Windows named pipe) lazily, exactly like the rest of the SAGE ecosystem. |
10
+ | **In-process** *(deferred to Phase 4)* | `wstack sage-mcp ...` *inside* the existing CLI | Strictly additive, opt-in only; preserves the rule "*without changing Sage's existing normal behavior*". |
11
+
12
+ This package does **not** introduce a new database, a new file format, or a new SQLite owner. The single-owner-of-state model in `packages/sage/src/project-server.ts` is preserved exactly: SAGE has one writer per project, and clients attach to it. MCP is one more client shape.
13
+
14
+ ## Install
15
+
16
+ Built artifacts come from `pnpm -F @wrongstack/sage-mcp build`. The CLI binary lives at `dist/cli.js` and is published as `wstack-sage-mcp`.
17
+
18
+ ```sh
19
+ pnpm install
20
+ pnpm -F @wrongstack/sage-mcp build
21
+ ```
22
+
23
+ ## Quick start — Claude Desktop
24
+
25
+ Save this as your Claude Desktop MCP config (path: `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "sage": {
31
+ "command": "wstack-sage-mcp",
32
+ "args": [
33
+ "--project-root",
34
+ "/absolute/path/to/your/project"
35
+ ]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Replace `/absolute/path/to/your/project` with the directory where you've been running `wstack`. Restart Claude Desktop; the SAGE tools appear in the tool picker.
42
+
43
+ ### Expose writes to the model
44
+
45
+ Default policy is **read-only** (`memory_search`, `memory_for_file`, `memory_for_path`, `memory_graph`). For an agent that should also `remember`, `forget`, or `memory_delete`:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "sage": {
51
+ "command": "wstack-sage-mcp",
52
+ "args": [
53
+ "--project-root",
54
+ "/absolute/path/to/your/project",
55
+ "--writable"
56
+ ]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ Claude Desktop will surface its own confirmation gesture before calling write-class tools. The Sage `force: true` gate on `memory_delete` is preserved at the MCP boundary — see *Safety guarantees* below.
63
+
64
+ ### Loopback HTTP (advanced)
65
+
66
+ If you want multiple clients to talk to the same SAGE project (e.g. two IDEs, one Claude Desktop, one custom script):
67
+
68
+ ```sh
69
+ wstack-sage-mcp \
70
+ --project-root /absolute/path/to/project \
71
+ --http \
72
+ --port 8765
73
+ ```
74
+
75
+ Then point any MCP HTTP client at `http://127.0.0.1:8765/`. Non-loopback hosts require `--token`:
76
+
77
+ ```sh
78
+ wstack-sage-mcp \
79
+ --project-root /absolute/path/to/project \
80
+ --http --host 0.0.0.0 --port 8765 --token "$(openssl rand -hex 32)"
81
+ ```
82
+
83
+ The HTTP transport reuses `serveHttp` from `@wrongstack/mcp` directly; the same loopback-default + token-required gate that `wstack mcp serve` enforces applies here.
84
+
85
+ ## CLI reference
86
+
87
+ ```
88
+ wstack-sage-mcp --project-root <path> [options]
89
+
90
+ Options:
91
+ --project-root <path> Project root whose SAGE memory should be served (required).
92
+ --storage-dir <path> Override the SAGE storage directory.
93
+ --stdio Use stdio transport (default).
94
+ --http Use HTTP transport.
95
+ --port <n> TCP port for HTTP mode (default 0 = ephemeral).
96
+ --host <h> Bind host for HTTP mode (default 127.0.0.1; non-loopback REQUIRES --token).
97
+ --token <t> Bearer token for HTTP mode.
98
+ --writable Expose standard-tier (write/delete) tools.
99
+ -h, --help Show this message.
100
+ ```
101
+
102
+ ## Tool allowlist
103
+
104
+ | Policy | Tools exposed |
105
+ |--------|---------------|
106
+ | **Default (read-only)** | `memory_search`, `memory_for_file`, `memory_for_path`, `memory_graph` |
107
+ | **`--writable`** adds | `remember`, `forget`, `memory_delete`, `memory_update`, `memory_recover`, `memory_backfill_recoverable`, `memory_verify`, `memory_hygiene`, `memory_candidates` |
108
+
109
+ `selectAllowedTools` is implemented at `src/policy.ts` and is fully unit-tested. The model is: `permission === 'deny'` is always excluded; `permission === 'auto'` is exposed under any risk tier when `--writable`; `permission === 'confirm'` is exposed only under `--writable`; `riskTier === 'destructive'` is **never** exposed (no current Sage tool is tagged destructive — this is forward-compatible).
110
+
111
+ ## Safety guarantees
112
+
113
+ - **`memory_delete` requires `force: true`** — same gate as `packages/sage/src/tools/memory-tools.ts:332-340`. MCP clients calling without `force` receive an `isError: true` JSON-RPC response, not a deletion.
114
+ - **`memory_update` validates** that at least one field is supplied.
115
+ - **`memory_verify`** mutates anchor state but accepts only known memory ids.
116
+ - **Audit log**: deletes are recorded in `readAudit` (use `Sage.audit` capability if you wire one in; tools don't expose this directly).
117
+ - **Loopback default for HTTP** — `serveHttp` refuses non-loopback binds without `--token`, matching `wstack mcp serve`.
118
+ - **Single-owner-of-SQLite** preserved — the standalone binary connects via `ProjectSageMemoryPort`, which lazily spawns `project-server.js` from `@wrongstack/sage` and never opens its own SQLite handle on a running project.
119
+
120
+ ## Verifying your install
121
+
122
+ ```sh
123
+ pnpm -F @wrongstack/sage-mcp tsx scripts/smoke.ts
124
+ ```
125
+
126
+ This boots the MCP server in-process and exercises all 8 protocol-level invariants (initialize / tools/list / memory_search / force-gate on `memory_delete` / malformed envelopes / method-not-found / unknown-tool). Exits 0 on full pass, non-zero with a precise message on the first failed assertion. **The smoke is host-independent** — it uses Node `PassThrough` streams instead of child-process spawn, so it works on Windows, Linux, and macOS.
127
+
128
+ ## Architecture
129
+
130
+ ```
131
+ ┌─────────────────────────────────────────────────────────────────┐
132
+ │ MCP client (Claude Desktop, custom agent, IDE) │
133
+ │ ↕ stdio / http │
134
+ └─────────────────────────────────────────────────────────────────┘
135
+
136
+
137
+ ┌─────────────────────────────────────────────────────────────────┐
138
+ │ wstack-sage-mcp │
139
+ │ • MCPServer.handleMessage ← from @wrongstack/mcp │
140
+ │ • serveStdio / serveHttp ← from @wrongstack/mcp │
141
+ │ • createSageMcpToolHost ← src/adapter.ts │
142
+ │ listTools = createSageTools(port) filtered by policy │
143
+ │ callTool = Tool.validate → Tool.execute → JSON content │
144
+ └─────────────────────────────────────────────────────────────────┘
145
+
146
+
147
+ ┌─────────────────────────────────────────────────────────────────┐
148
+ │ ProjectSageMemoryPort │
149
+ │ (reconnecting IPC client; transparently acquires the SAGE │
150
+ │ project server if not already running) │
151
+ └─────────────────────────────────────────────────────────────────┘
152
+
153
+
154
+ ┌─────────────────────────────────────────────────────────────────┐
155
+ │ SAGE project server (existing; packages/sage/src/project-server.ts) │
156
+ │ Unix socket / Windows named pipe — single owner of SQLite │
157
+ └─────────────────────────────────────────────────────────────────┘
158
+
159
+
160
+ ┌─────────────────────────────────────────────────────────────────┐
161
+ │ node:sqlite DatabaseSync (WAL) │
162
+ └─────────────────────────────────────────────────────────────────┘
163
+ ```
164
+
165
+ ## Public exports (`@wrongstack/sage-mcp`)
166
+
167
+ ```ts
168
+ import {
169
+ createSageMcpServer, // (port, opts) => MCPServer
170
+ createSageMcpToolHost, // (port, opts) => MCPServerToolHost
171
+ requireSageService, // (port) => SageServiceLike; throws if absent
172
+ selectAllowedTools, // (tools, opts) => filtered tools (for tests / custom policies)
173
+ type SageMcpToolHostOptions,
174
+ type SageMcpPolicyOptions,
175
+ type SageMcpAllowedTool,
176
+ } from '@wrongstack/sage-mcp';
177
+ ```
178
+
179
+ ## Related packages
180
+
181
+ - `@wrongstack/sage` — implementation owner of memory; provides `createSageTools`, `SqliteMemoryPort`, `ProjectSageMemoryPort`.
182
+ - `@wrongstack/mcp` — MCP toolkit; provides `MCPServer`, `serveStdio`, `serveHttp`, `MCPRegistry`, `transport-security`. This package reuses those primitives; it does not extend them.
183
+ - `@wrongstack/kanban` — the kanban daemon follows the same single-owner-of-state, many-clients-over-IPC architecture. Same pattern, different data.
184
+
185
+ ## License
186
+
187
+ MIT — see repository root.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * MCPServerToolHost adapter for SAGE Memory.
3
+ *
4
+ * Maps `createSageTools(port)` from `@wrongstack/sage` into the
5
+ * transport-agnostic `MCPServerToolHost` shape expected by
6
+ * `@wrongstack/mcp`'s `MCPServer` (`packages/mcp/src/server.ts:36-39`).
7
+ *
8
+ * Tool-level safety gates (e.g. `force: true` on `memory_delete` from
9
+ * `packages/sage/src/tools/memory-tools.ts:332-340`) are preserved by passing
10
+ * the Sage tool's `Tool.validate` + `Tool.execute` through unchanged —
11
+ * a Sage `validate` failure surfaces as `isError: true` MCP content,
12
+ * the same end-shape `wstack mcp serve` already returns in
13
+ * `packages/cli/src/mcp-serve.ts:298-318`.
14
+ *
15
+ * The synthetic `Context` mirrors `packages/cli/src/mcp-serve.ts:179-214`
16
+ * (`makeServeContext`). It has no provider, no session, no tokenCounter.
17
+ * SAGE's `remember` tool reads `ctx.meta['agentRole']` /
18
+ * `ctx.meta['mode']` to auto-scope audience
19
+ * (`packages/sage/src/tools/memory-tools.ts:166-178`); we force
20
+ * `arguments.no_auto_audience = true` so MCP callers must opt in explicitly.
21
+ */
22
+ import { MCPServer, type MCPServerToolHost } from '@wrongstack/mcp';
23
+ import type { MemoryPort } from '@wrongstack/core/types';
24
+ import { type SageServiceLike } from '@wrongstack/sage';
25
+ import { type SageMcpPolicyOptions } from './policy.js';
26
+ export interface SageMcpToolHostOptions extends SageMcpPolicyOptions {
27
+ }
28
+ /**
29
+ * Resolve the SAGE service capability from any `MemoryPort`. Both
30
+ * `SqliteMemoryPort` and `ProjectSageMemoryPort` expose it via
31
+ * `getCapability(SAGE_SERVICE_CAPABILITY)`; if either is missing,
32
+ * we throw a precise error.
33
+ *
34
+ * The capability lookup is the same shape
35
+ * `packages/runtime/src/container.ts:131-152` already relies on for
36
+ * wiring the IPC bound port into the tool registry.
37
+ */
38
+ export declare function requireSageService(port: MemoryPort): SageServiceLike;
39
+ export declare function createSageMcpToolHost(port: MemoryPort, opts?: SageMcpToolHostOptions): MCPServerToolHost;
40
+ export declare function createSageMcpServer(port: MemoryPort, opts?: SageMcpToolHostOptions): MCPServer;
41
+ //# sourceMappingURL=adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EACL,SAAS,EAGT,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAc,UAAU,EAAQ,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAsB,KAAK,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAG5E,MAAM,WAAW,sBAAuB,SAAQ,oBAAoB;CAAG;AAEvE;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,eAAe,CASpE;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,GAAE,sBAA2B,GAChC,iBAAiB,CAqFnB;AAiBD,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,UAAU,EAChB,IAAI,GAAE,sBAA2B,GAChC,SAAS,CAMX"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ interface ParsedArgs {
3
+ projectRoot: string;
4
+ storageDirectory?: string | undefined;
5
+ transport: 'stdio' | 'http';
6
+ httpPort: number;
7
+ httpHost: string;
8
+ httpToken?: string | undefined;
9
+ writable: boolean;
10
+ help: boolean;
11
+ }
12
+ export declare function parseArgs(argv: readonly string[]): ParsedArgs;
13
+ export {};
14
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAmCA,UAAU,UAAU;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,SAAS,EAAE,OAAO,GAAG,MAAM,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,OAAO,CAAC;CACf;AA4BD,wBAAgB,SAAS,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,UAAU,CA8C7D"}
package/dist/cli.js ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { realpathSync } from "node:fs";
5
+ import * as path from "node:path";
6
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7
+ import { canonicalProjectRoot } from "@wrongstack/core/utils";
8
+ import { serveHttp, serveStdio } from "@wrongstack/mcp";
9
+ import {
10
+ ProjectSageMemoryPort,
11
+ isSageProjectServerAvailable
12
+ } from "@wrongstack/sage";
13
+
14
+ // src/adapter.ts
15
+ import {
16
+ MCPServer
17
+ } from "@wrongstack/mcp";
18
+ import {
19
+ createSageTools,
20
+ getSageService,
21
+ isSqliteAvailable
22
+ } from "@wrongstack/sage";
23
+
24
+ // src/policy.ts
25
+ function selectAllowedTools(tools, opts = {}) {
26
+ const allowed = [];
27
+ for (const tool of tools) {
28
+ if (tool.permission === "deny") continue;
29
+ if (tool.riskTier === "destructive") continue;
30
+ if (tool.permission === "auto") {
31
+ if (tool.riskTier === "safe" || opts.writable === true) {
32
+ allowed.push({ name: tool.name, tool });
33
+ }
34
+ continue;
35
+ }
36
+ if (opts.writable !== true) continue;
37
+ if (tool.riskTier !== "standard" && tool.riskTier !== "safe") continue;
38
+ allowed.push({ name: tool.name, tool });
39
+ }
40
+ return allowed;
41
+ }
42
+
43
+ // src/version.ts
44
+ import { readFileSync } from "node:fs";
45
+ import { fileURLToPath } from "node:url";
46
+ import { dirname, resolve } from "node:path";
47
+ var here = dirname(fileURLToPath(import.meta.url));
48
+ var pkgPath = resolve(here, "..", "package.json");
49
+ var cached;
50
+ function readServerInfo() {
51
+ if (cached) return cached;
52
+ try {
53
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
54
+ cached = {
55
+ name: pkg.name ?? "@wrongstack/sage-mcp",
56
+ version: pkg.version ?? "0.0.0"
57
+ };
58
+ return cached;
59
+ } catch {
60
+ cached = { name: "@wrongstack/sage-mcp", version: "0.0.0" };
61
+ return cached;
62
+ }
63
+ }
64
+ var SERVER_INFO = readServerInfo();
65
+
66
+ // src/adapter.ts
67
+ function requireSageService(port) {
68
+ const service = getSageService(port);
69
+ if (!service) {
70
+ throw new Error(
71
+ "SAGE MCP: the supplied MemoryPort does not expose the SAGE service capability. Use the port created by createProjectSageMemoryPort(...) or SqliteMemoryPort directly."
72
+ );
73
+ }
74
+ return service;
75
+ }
76
+ function createSageMcpToolHost(port, opts = {}) {
77
+ if (!isSqliteAvailable()) {
78
+ throw new Error(
79
+ "SAGE MCP: node:sqlite is unavailable; SAGE requires Node >= 22.5"
80
+ );
81
+ }
82
+ const service = requireSageService(port);
83
+ const allTools = createSageTools(service);
84
+ const allowed = selectAllowedTools(allTools, opts);
85
+ const allowedByName = new Map(allowed.map((entry) => [entry.name, entry.tool]));
86
+ const ctx = createSyntheticContext();
87
+ const ac = new AbortController();
88
+ const signal = ac.signal;
89
+ function jsonSchemaToObject(schema) {
90
+ return schema;
91
+ }
92
+ return {
93
+ listTools() {
94
+ return allowed.map(
95
+ ({ name, tool }) => ({
96
+ name,
97
+ ...tool.description ? { description: tool.description } : {},
98
+ inputSchema: jsonSchemaToObject(tool.inputSchema)
99
+ })
100
+ );
101
+ },
102
+ async callTool(name, args) {
103
+ const tool = allowedByName.get(name);
104
+ if (!tool) {
105
+ return {
106
+ content: `Tool "${name}" is not exposed by this SAGE MCP server`,
107
+ isError: true
108
+ };
109
+ }
110
+ const callArgs = { ...args };
111
+ if (name === "remember" && callArgs["no_auto_audience"] === void 0) {
112
+ callArgs["no_auto_audience"] = true;
113
+ }
114
+ const validate = tool.validate;
115
+ if (typeof validate === "function") {
116
+ const errors = await validate(callArgs);
117
+ if (Array.isArray(errors) && errors.length > 0) {
118
+ return { content: errors.join("\n"), isError: true };
119
+ }
120
+ }
121
+ let output;
122
+ try {
123
+ output = await tool.execute(callArgs, ctx, { signal });
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ return { content: message, isError: true };
127
+ }
128
+ if (typeof output === "string") return { content: output, isError: false };
129
+ return { content: output, isError: false };
130
+ }
131
+ };
132
+ }
133
+ function createSyntheticContext() {
134
+ return {
135
+ systemPrompt: [],
136
+ cwd: process.cwd(),
137
+ projectRoot: process.cwd(),
138
+ allowOutsideProjectRoot: false,
139
+ model: "sage-mcp",
140
+ tools: [],
141
+ meta: {}
142
+ };
143
+ }
144
+ function createSageMcpServer(port, opts = {}) {
145
+ const host = createSageMcpToolHost(port, opts);
146
+ return new MCPServer({
147
+ host,
148
+ serverInfo: { name: "wrongstack-sage-mcp", version: SERVER_INFO.version }
149
+ });
150
+ }
151
+
152
+ // src/cli.ts
153
+ function printHelp(stdout) {
154
+ stdout.write(
155
+ [
156
+ `${SERVER_INFO.name} v${SERVER_INFO.version} \u2014 SAGE Memory MCP server`,
157
+ "",
158
+ "Usage:",
159
+ ` ${SERVER_INFO.name} --project-root <path> [options]`,
160
+ "",
161
+ "Options:",
162
+ " --project-root <path> Project root whose SAGE memory should be served (required).",
163
+ " --storage-dir <path> Override the SAGE storage directory.",
164
+ " --stdio Use stdio transport (default).",
165
+ " --http Use HTTP transport.",
166
+ " --port <n> TCP port for HTTP mode (default 0 = ephemeral).",
167
+ " --host <h> Bind host for HTTP mode (default 127.0.0.1;",
168
+ " non-loopback REQUIRES --token, refused by serveHttp).",
169
+ " --token <t> Bearer token for HTTP mode.",
170
+ " --writable Expose standard-tier (write/delete) tools.",
171
+ " -h, --help Show this message.",
172
+ "",
173
+ "Existing SAGE IPC server connection:",
174
+ ` isSageProjectServerAvailable() = ${String(isSageProjectServerAvailable())}`
175
+ ].join("\n") + "\n"
176
+ );
177
+ }
178
+ function parseArgs(argv) {
179
+ const out = {
180
+ projectRoot: "",
181
+ transport: "stdio",
182
+ httpPort: 0,
183
+ httpHost: "127.0.0.1",
184
+ writable: false,
185
+ help: false
186
+ };
187
+ for (let i = 0; i < argv.length; i++) {
188
+ const arg = argv[i];
189
+ switch (arg) {
190
+ case "--project-root":
191
+ out.projectRoot = path.resolve(argv[++i] ?? "");
192
+ break;
193
+ case "--storage-dir":
194
+ out.storageDirectory = path.resolve(argv[++i] ?? "");
195
+ break;
196
+ case "--stdio":
197
+ out.transport = "stdio";
198
+ break;
199
+ case "--http":
200
+ out.transport = "http";
201
+ break;
202
+ case "--port":
203
+ out.httpPort = Number(argv[++i] ?? "") || 0;
204
+ break;
205
+ case "--host":
206
+ out.httpHost = argv[++i] ?? "127.0.0.1";
207
+ break;
208
+ case "--token":
209
+ out.httpToken = argv[++i];
210
+ break;
211
+ case "--writable":
212
+ out.writable = true;
213
+ break;
214
+ case "-h":
215
+ case "--help":
216
+ out.help = true;
217
+ break;
218
+ default:
219
+ break;
220
+ }
221
+ }
222
+ return out;
223
+ }
224
+ async function main() {
225
+ const args = parseArgs(process.argv.slice(2));
226
+ if (args.help) {
227
+ printHelp(process.stdout);
228
+ return 0;
229
+ }
230
+ if (!args.projectRoot) {
231
+ process.stderr.write(`${SERVER_INFO.name}: --project-root is required
232
+ `);
233
+ printHelp(process.stderr);
234
+ return 2;
235
+ }
236
+ const projectRoot = canonicalProjectRoot(args.projectRoot);
237
+ const port = new ProjectSageMemoryPort({
238
+ projectRoot,
239
+ ...args.storageDirectory ? { directory: args.storageDirectory } : {}
240
+ });
241
+ try {
242
+ await port.initialize();
243
+ } catch (error) {
244
+ process.stderr.write(
245
+ `${SERVER_INFO.name}: cannot attach to SAGE project server for ${projectRoot}: ` + (error instanceof Error ? error.message : String(error)) + "\n"
246
+ );
247
+ return 3;
248
+ }
249
+ const server = createSageMcpServer(port, { writable: args.writable });
250
+ if (args.transport === "http") {
251
+ const handle2 = await serveHttp(server, {
252
+ port: args.httpPort,
253
+ host: args.httpHost,
254
+ ...args.httpToken ? { token: args.httpToken } : {},
255
+ logger: { warn: (m) => process.stderr.write(`[sage-mcp] ${m}
256
+ `) }
257
+ });
258
+ process.stderr.write(
259
+ `${SERVER_INFO.name}: ready at ${handle2.url} \u2014 projectRoot=${projectRoot} transport=http writable=${String(args.writable)}${args.httpToken ? " [token auth]" : ""}
260
+ `
261
+ );
262
+ return await new Promise((resolve3) => {
263
+ const stop = () => resolve3(0);
264
+ process.once("SIGINT", stop);
265
+ process.once("SIGTERM", stop);
266
+ });
267
+ }
268
+ const handle = serveStdio(server);
269
+ process.stderr.write(
270
+ `${SERVER_INFO.name}: ready on stdio \u2014 projectRoot=${projectRoot} transport=stdio writable=${String(args.writable)}
271
+ `
272
+ );
273
+ await handle.done;
274
+ await port.dispose();
275
+ return 0;
276
+ }
277
+ function isMainModule() {
278
+ const entry = process.argv[1];
279
+ if (!entry) return false;
280
+ const self = fileURLToPath2(import.meta.url);
281
+ if (path.resolve(entry) === self) return true;
282
+ try {
283
+ return realpathSync(entry) === realpathSync(self);
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
288
+ if (isMainModule()) {
289
+ main().then(
290
+ (code) => {
291
+ process.exitCode = code;
292
+ },
293
+ (error) => {
294
+ process.stderr.write(`${SERVER_INFO.name}: unexpected error
295
+ `);
296
+ process.stderr.write(error instanceof Error ? error.stack ?? error.message : String(error));
297
+ process.stderr.write("\n");
298
+ process.exitCode = 1;
299
+ }
300
+ );
301
+ }
302
+ export {
303
+ parseArgs
304
+ };
305
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cli.ts", "../src/adapter.ts", "../src/policy.ts", "../src/version.ts"],
4
+ "sourcesContent": ["#!/usr/bin/env node\n/**\n * `wstack-sage-mcp` \u2014 Standalone MCP server for WrongStack SAGE Memory.\n *\n * Two operating modes (default stdio; --http for loopback HTTP):\n *\n * $ wstack-sage-mcp --project-root <path>\n * # talks JSON-RPC over stdio\n *\n * $ wstack-sage-mcp --project-root <path> --http --port 8765\n * # loopback HTTP; refuses non-loopback without --token\n *\n * The memory port is acquired by connecting to the existing SAGE IPC\n * project server (Unix socket / Windows named pipe). If no server is\n * running, the connection logic in `SageProjectServerConnection`\n * (`packages/sage/src/project-server-client.ts:233-275`) lazily spawns\n * `project-server.js` from `@wrongstack/sage` \u2014 the same single-owner\n * SQLite process that any wstack CLI / TUI / WebUI would attach to.\n *\n * Default tool policy: read-only (`permission === 'auto'`,\n * `riskTier === 'safe'`). Pass `--writable` to expose standard-tier\n * Sage tools (writes, deletes, hygiene).\n */\nimport { realpathSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { canonicalProjectRoot } from '@wrongstack/core/utils';\nimport { serveHttp, serveStdio } from '@wrongstack/mcp';\nimport {\n ProjectSageMemoryPort,\n isSageProjectServerAvailable,\n} from '@wrongstack/sage';\nimport { createSageMcpServer } from './adapter.js';\nimport { SERVER_INFO } from './version.js';\n\ninterface ParsedArgs {\n projectRoot: string;\n storageDirectory?: string | undefined;\n transport: 'stdio' | 'http';\n httpPort: number;\n httpHost: string;\n httpToken?: string | undefined;\n writable: boolean;\n help: boolean;\n}\n\nfunction printHelp(stdout: NodeJS.WriteStream): void {\n stdout.write(\n [\n `${SERVER_INFO.name} v${SERVER_INFO.version} \u2014 SAGE Memory MCP server`,\n '',\n 'Usage:',\n ` ${SERVER_INFO.name} --project-root <path> [options]`,\n '',\n 'Options:',\n ' --project-root <path> Project root whose SAGE memory should be served (required).',\n ' --storage-dir <path> Override the SAGE storage directory.',\n ' --stdio Use stdio transport (default).',\n ' --http Use HTTP transport.',\n ' --port <n> TCP port for HTTP mode (default 0 = ephemeral).',\n ' --host <h> Bind host for HTTP mode (default 127.0.0.1;',\n ' non-loopback REQUIRES --token, refused by serveHttp).',\n ' --token <t> Bearer token for HTTP mode.',\n ' --writable Expose standard-tier (write/delete) tools.',\n ' -h, --help Show this message.',\n '',\n 'Existing SAGE IPC server connection:',\n ` isSageProjectServerAvailable() = ${String(isSageProjectServerAvailable())}`,\n ].join('\\n') + '\\n',\n );\n}\n\nexport function parseArgs(argv: readonly string[]): ParsedArgs {\n const out: ParsedArgs = {\n projectRoot: '',\n transport: 'stdio',\n httpPort: 0,\n httpHost: '127.0.0.1',\n writable: false,\n help: false,\n };\n for (let i = 0; i < argv.length; i++) {\n const arg = argv[i];\n switch (arg) {\n case '--project-root':\n out.projectRoot = path.resolve(argv[++i] ?? '');\n break;\n case '--storage-dir':\n out.storageDirectory = path.resolve(argv[++i] ?? '');\n break;\n case '--stdio':\n out.transport = 'stdio';\n break;\n case '--http':\n out.transport = 'http';\n break;\n case '--port':\n out.httpPort = Number(argv[++i] ?? '') || 0;\n break;\n case '--host':\n out.httpHost = argv[++i] ?? '127.0.0.1';\n break;\n case '--token':\n out.httpToken = argv[++i];\n break;\n case '--writable':\n out.writable = true;\n break;\n case '-h':\n case '--help':\n out.help = true;\n break;\n default:\n // ignore unknown flags (forward-compat)\n break;\n }\n }\n return out;\n}\n\nasync function main(): Promise<number> {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) {\n printHelp(process.stdout);\n return 0;\n }\n if (!args.projectRoot) {\n process.stderr.write(`${SERVER_INFO.name}: --project-root is required\\n`);\n printHelp(process.stderr);\n return 2;\n }\n\n const projectRoot = canonicalProjectRoot(args.projectRoot);\n const port = new ProjectSageMemoryPort({\n projectRoot,\n ...(args.storageDirectory ? { directory: args.storageDirectory } : {}),\n });\n\n // Acquire (or lazily spawn) the SAGE IPC server. We don't lower this to\n // a try/catch around `port.initialize()` because the connection logic\n // already surfaces a precise message via `isSageProjectServerAvailable()`.\n try {\n await port.initialize();\n } catch (error) {\n process.stderr.write(\n `${SERVER_INFO.name}: cannot attach to SAGE project server for ${projectRoot}: ` +\n (error instanceof Error ? error.message : String(error)) +\n '\\n',\n );\n return 3;\n }\n\n const server = createSageMcpServer(port, { writable: args.writable });\n\n if (args.transport === 'http') {\n const handle = await serveHttp(server, {\n port: args.httpPort,\n host: args.httpHost,\n ...(args.httpToken ? { token: args.httpToken } : {}),\n logger: { warn: (m) => process.stderr.write(`[sage-mcp] ${m}\\n`) },\n });\n process.stderr.write(\n `${SERVER_INFO.name}: ready at ${handle.url} \u2014 projectRoot=${projectRoot} ` +\n `transport=http writable=${String(args.writable)}${args.httpToken ? ' [token auth]' : ''}\\n`,\n );\n return await new Promise<number>((resolve) => {\n const stop = () => resolve(0);\n process.once('SIGINT', stop);\n process.once('SIGTERM', stop);\n });\n }\n\n // stdio mode\n const handle = serveStdio(server);\n process.stderr.write(\n `${SERVER_INFO.name}: ready on stdio \u2014 projectRoot=${projectRoot} ` +\n `transport=stdio writable=${String(args.writable)}\\n`,\n );\n await handle.done;\n await port.dispose();\n return 0;\n}\n\n/**\n * Only run the CLI when this module IS the process entry point.\n *\n * `tests/cli.test.ts` imports `parseArgs` from here. Without this guard the\n * import ran `main()` inside the Vitest worker: Vitest's argv carries no\n * `--project-root`, so `main` printed the help block and set\n * `process.exitCode = 2`. The worker fork then exited mid-run and the pool's\n * next `send()` failed with `EPIPE` \u2014 surfacing as a single unhandled\n * \"Worker forks emitted error\" that failed the whole suite while every\n * individual test still passed.\n *\n * The comparison has to be real-path based: `file://${process.argv[1]}` never\n * matches on Windows (argv[1] is a backslash path, `import.meta.url` is a\n * percent-encoded forward-slash URL), and on POSIX argv[1] is the\n * `node_modules/.bin/wstack-sage-mcp` symlink rather than `dist/cli.js`.\n */\nfunction isMainModule(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n const self = fileURLToPath(import.meta.url);\n if (path.resolve(entry) === self) return true;\n try {\n return realpathSync(entry) === realpathSync(self);\n } catch {\n return false;\n }\n}\n\nif (isMainModule()) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error) => {\n process.stderr.write(`${SERVER_INFO.name}: unexpected error\\n`);\n process.stderr.write(error instanceof Error ? (error.stack ?? error.message) : String(error));\n process.stderr.write('\\n');\n process.exitCode = 1;\n },\n );\n}\n", "/**\n * MCPServerToolHost adapter for SAGE Memory.\n *\n * Maps `createSageTools(port)` from `@wrongstack/sage` into the\n * transport-agnostic `MCPServerToolHost` shape expected by\n * `@wrongstack/mcp`'s `MCPServer` (`packages/mcp/src/server.ts:36-39`).\n *\n * Tool-level safety gates (e.g. `force: true` on `memory_delete` from\n * `packages/sage/src/tools/memory-tools.ts:332-340`) are preserved by passing\n * the Sage tool's `Tool.validate` + `Tool.execute` through unchanged \u2014\n * a Sage `validate` failure surfaces as `isError: true` MCP content,\n * the same end-shape `wstack mcp serve` already returns in\n * `packages/cli/src/mcp-serve.ts:298-318`.\n *\n * The synthetic `Context` mirrors `packages/cli/src/mcp-serve.ts:179-214`\n * (`makeServeContext`). It has no provider, no session, no tokenCounter.\n * SAGE's `remember` tool reads `ctx.meta['agentRole']` /\n * `ctx.meta['mode']` to auto-scope audience\n * (`packages/sage/src/tools/memory-tools.ts:166-178`); we force\n * `arguments.no_auto_audience = true` so MCP callers must opt in explicitly.\n */\nimport {\n MCPServer,\n type MCPServerCallResult,\n type MCPServerTool,\n type MCPServerToolHost,\n} from '@wrongstack/mcp';\nimport type { Context } from '@wrongstack/core/agent';\nimport type { JSONSchema, MemoryPort, Tool } from '@wrongstack/core/types';\nimport {\n type SageServiceLike,\n createSageTools,\n getSageService,\n isSqliteAvailable,\n} from '@wrongstack/sage';\nimport { selectAllowedTools, type SageMcpPolicyOptions } from './policy.js';\nimport { SERVER_INFO } from './version.js';\n\nexport interface SageMcpToolHostOptions extends SageMcpPolicyOptions {}\n\n/**\n * Resolve the SAGE service capability from any `MemoryPort`. Both\n * `SqliteMemoryPort` and `ProjectSageMemoryPort` expose it via\n * `getCapability(SAGE_SERVICE_CAPABILITY)`; if either is missing,\n * we throw a precise error.\n *\n * The capability lookup is the same shape\n * `packages/runtime/src/container.ts:131-152` already relies on for\n * wiring the IPC bound port into the tool registry.\n */\nexport function requireSageService(port: MemoryPort): SageServiceLike {\n const service = getSageService(port);\n if (!service) {\n throw new Error(\n 'SAGE MCP: the supplied MemoryPort does not expose the SAGE service capability. ' +\n 'Use the port created by createProjectSageMemoryPort(...) or SqliteMemoryPort directly.',\n );\n }\n return service;\n}\n\nexport function createSageMcpToolHost(\n port: MemoryPort,\n opts: SageMcpToolHostOptions = {},\n): MCPServerToolHost {\n if (!isSqliteAvailable()) {\n // The same gate as `packages/runtime/src/container.ts:139-144` \u2014\n // SAGE requires Node >= 22.5 with `node:sqlite`. Fail loudly here\n // instead of letting the first `Tool.execute` crash mid-protocol.\n throw new Error(\n 'SAGE MCP: node:sqlite is unavailable; SAGE requires Node >= 22.5',\n );\n }\n\n const service = requireSageService(port);\n const allTools: Tool[] = createSageTools(service);\n const allowed = selectAllowedTools(allTools, opts);\n const allowedByName = new Map<string, Tool>(allowed.map((entry) => [entry.name, entry.tool]));\n\n // Synthetic Context \u2014 no provider / session / tokenCounter. SAGE-only memory\n // ops never read those fields; `ctx.meta` is empty, which makes\n // `no_auto_audience = true` (set below) both deterministic and defensible.\n const ctx = createSyntheticContext();\n const ac = new AbortController();\n const signal = ac.signal;\n\n function jsonSchemaToObject(schema: JSONSchema): Record<string, unknown> {\n // JSONSchema's permissive `[k: string]: unknown` already satisfies the\n // MCP `Record<string, unknown>` inputSchema contract. We only narrow\n // the top-level return type for the strict MCPServerTool type.\n return schema as unknown as Record<string, unknown>;\n }\n\n return {\n listTools(): MCPServerTool[] {\n return allowed.map(\n ({ name, tool }): MCPServerTool => ({\n name,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: jsonSchemaToObject(tool.inputSchema),\n }),\n );\n },\n\n async callTool(name: string, args: Record<string, unknown>): Promise<MCPServerCallResult> {\n const tool = allowedByName.get(name);\n if (!tool) {\n return {\n content: `Tool \"${name}\" is not exposed by this SAGE MCP server`,\n isError: true,\n };\n }\n\n // For `remember`, force no_auto_audience=true. SAGE's auto-audience\n // detection reads `ctx.meta['agentRole']`/`ctx.meta['mode']`; MCP\n // callers do not supply a role/mode so we cannot infer one.\n const callArgs: Record<string, unknown> = { ...args };\n if (name === 'remember' && callArgs['no_auto_audience'] === undefined) {\n callArgs['no_auto_audience'] = true;\n }\n\n // Tool.validate is the canonical safety gate (e.g., the `force: true`\n // requirement on memory_delete). Surface validation failures exactly\n // like `wstack mcp serve` would.\n const validate = tool.validate;\n if (typeof validate === 'function') {\n const errors = await validate(callArgs);\n if (Array.isArray(errors) && errors.length > 0) {\n return { content: errors.join('\\n'), isError: true };\n }\n }\n\n let output: unknown;\n try {\n output = await tool.execute(callArgs, ctx, { signal });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { content: message, isError: true };\n }\n\n // The output shape mirrors how `wstack mcp serve` returns its\n // executor result (a string, see packages/cli/src/mcp-serve.ts:316).\n // Sage tools return strings, structured objects, or arrays; we let\n // `MCPServer.toContentBlocks` (server.ts:303-316) handle the\n // text-block wrapping uniformly.\n if (typeof output === 'string') return { content: output, isError: false };\n return { content: output as unknown, isError: false };\n },\n };\n}\n\nfunction createSyntheticContext(): Context {\n // Minimal Context stand-in. SAGE memory ops only read `ctx.meta`; the\n // provider/session/tokenCounter stubs throw on use, which is desired\n // because MCP-side memory calls are pure data operations.\n return {\n systemPrompt: [],\n cwd: process.cwd(),\n projectRoot: process.cwd(),\n allowOutsideProjectRoot: false,\n model: 'sage-mcp',\n tools: [],\n meta: {},\n } as unknown as Context;\n}\n\nexport function createSageMcpServer(\n port: MemoryPort,\n opts: SageMcpToolHostOptions = {},\n): MCPServer {\n const host = createSageMcpToolHost(port, opts);\n return new MCPServer({\n host,\n serverInfo: { name: 'wrongstack-sage-mcp', version: SERVER_INFO.version },\n });\n}\n", "/**\n * Tool allowlist policy for the SAGE MCP server.\n *\n * Mirrors `packages/cli/src/mcp-serve.ts:217-236` (selectExposedTools).\n *\n * Tool permission model:\n * - Sage tools use `permission: 'confirm'` for any write/delete/update/\n * recover/backfill/verify/hygiene operation\n * (`packages/sage/src/tools/memory-tools.ts`), reserving\n * `permission: 'auto'` for read-only tools.\n * - `wstack mcp serve` (`packages/cli/src/mcp-serve.ts:217-236`)\n * decides auto-approval by routing through a host `PermissionPolicy`\n * (`AutoApprovePermissionPolicy` defaults to read-only).\n *\n * SAGE MCP chooses a simpler axis: MCP has no UI confirm flow, so a write\n * tool surfaced over MCP cannot be approved inline \u2014 the MCP client (e.g.\n * Claude Desktop) is expected to surface its own confirm UX before\n * forwarding the call. Default policy is therefore \"read-only\", exposing\n * only `permission: 'auto'` AND `riskTier === 'safe'` tools. Pass\n * `--writable` to additionally expose `permission === 'confirm'` tools\n * whose `riskTier !== 'destructive'`. The MCP client owns the user-facing\n * confirmation gesture in that case.\n *\n * The tool's own `Tool.validate(input)` (see\n * `packages/sage/src/tools/memory-tools.ts:276-283` and `:332-340`) remains\n * the source of truth for per-call safety checks such as `force: true` on\n * `memory_delete`. Policy only controls visibility.\n */\nimport type { Tool } from '@wrongstack/core/types';\n\nexport interface SageMcpPolicyOptions {\n writable?: boolean;\n}\n\nexport interface SageMcpAllowedTool {\n name: string;\n tool: Tool;\n}\n\nexport function selectAllowedTools(\n tools: Tool[],\n opts: SageMcpPolicyOptions = {},\n): SageMcpAllowedTool[] {\n const allowed: SageMcpAllowedTool[] = [];\n for (const tool of tools) {\n if (tool.permission === 'deny') continue;\n if (tool.riskTier === 'destructive') continue;\n if (tool.permission === 'auto') {\n if (tool.riskTier === 'safe' || opts.writable === true) {\n allowed.push({ name: tool.name, tool });\n }\n continue;\n }\n // permission === 'confirm'\n if (opts.writable !== true) continue;\n if (tool.riskTier !== 'standard' && tool.riskTier !== 'safe') continue;\n allowed.push({ name: tool.name, tool });\n }\n return allowed;\n}\n", "/**\n * Single source of truth for the package version reported to MCP clients.\n * Kept tiny so a build step never has to run before `pnpm --filter` tests\n * can read it.\n */\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, resolve } from 'node:path';\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = resolve(here, '..', 'package.json');\n\ninterface MinimalPackage {\n name?: string;\n version?: string;\n}\n\nlet cached: { name: string; version: string } | undefined;\nfunction readServerInfo(): { name: string; version: string } {\n if (cached) return cached;\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as MinimalPackage;\n cached = {\n name: pkg.name ?? '@wrongstack/sage-mcp',\n version: pkg.version ?? '0.0.0',\n };\n return cached;\n } catch {\n cached = { name: '@wrongstack/sage-mcp', version: '0.0.0' };\n return cached;\n }\n}\n\nexport const SERVER_INFO = readServerInfo();\n"],
5
+ "mappings": "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",
6
+ "names": ["fileURLToPath", "handle", "resolve", "fileURLToPath"]
7
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Public exports of `@wrongstack/sage-mcp`.
3
+ *
4
+ * The two things a host needs are:
5
+ * - `createSageMcpServer(port, opts)` — build an `MCPServer` over a SAGE
6
+ * memory port. Pass the port, hand the result to `serveStdio` /
7
+ * `serveHttp` from `@wrongstack/mcp`.
8
+ * - `createSageMcpToolHost(port, opts)` — finer-grained: build just the
9
+ * host if you want to compose the server differently.
10
+ *
11
+ * The standalone `wstack-sage-mcp` binary lives at `src/cli.ts` (the
12
+ * `bin` entry in `package.json`); it is not re-exported here.
13
+ */
14
+ export { createSageMcpServer, createSageMcpToolHost, requireSageService, type SageMcpToolHostOptions, } from './adapter.js';
15
+ export { selectAllowedTools, type SageMcpAllowedTool, type SageMcpPolicyOptions, } from './policy.js';
16
+ export { SERVER_INFO } from './version.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,sBAAsB,GAC5B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,145 @@
1
+ // src/adapter.ts
2
+ import {
3
+ MCPServer
4
+ } from "@wrongstack/mcp";
5
+ import {
6
+ createSageTools,
7
+ getSageService,
8
+ isSqliteAvailable
9
+ } from "@wrongstack/sage";
10
+
11
+ // src/policy.ts
12
+ function selectAllowedTools(tools, opts = {}) {
13
+ const allowed = [];
14
+ for (const tool of tools) {
15
+ if (tool.permission === "deny") continue;
16
+ if (tool.riskTier === "destructive") continue;
17
+ if (tool.permission === "auto") {
18
+ if (tool.riskTier === "safe" || opts.writable === true) {
19
+ allowed.push({ name: tool.name, tool });
20
+ }
21
+ continue;
22
+ }
23
+ if (opts.writable !== true) continue;
24
+ if (tool.riskTier !== "standard" && tool.riskTier !== "safe") continue;
25
+ allowed.push({ name: tool.name, tool });
26
+ }
27
+ return allowed;
28
+ }
29
+
30
+ // src/version.ts
31
+ import { readFileSync } from "node:fs";
32
+ import { fileURLToPath } from "node:url";
33
+ import { dirname, resolve } from "node:path";
34
+ var here = dirname(fileURLToPath(import.meta.url));
35
+ var pkgPath = resolve(here, "..", "package.json");
36
+ var cached;
37
+ function readServerInfo() {
38
+ if (cached) return cached;
39
+ try {
40
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
41
+ cached = {
42
+ name: pkg.name ?? "@wrongstack/sage-mcp",
43
+ version: pkg.version ?? "0.0.0"
44
+ };
45
+ return cached;
46
+ } catch {
47
+ cached = { name: "@wrongstack/sage-mcp", version: "0.0.0" };
48
+ return cached;
49
+ }
50
+ }
51
+ var SERVER_INFO = readServerInfo();
52
+
53
+ // src/adapter.ts
54
+ function requireSageService(port) {
55
+ const service = getSageService(port);
56
+ if (!service) {
57
+ throw new Error(
58
+ "SAGE MCP: the supplied MemoryPort does not expose the SAGE service capability. Use the port created by createProjectSageMemoryPort(...) or SqliteMemoryPort directly."
59
+ );
60
+ }
61
+ return service;
62
+ }
63
+ function createSageMcpToolHost(port, opts = {}) {
64
+ if (!isSqliteAvailable()) {
65
+ throw new Error(
66
+ "SAGE MCP: node:sqlite is unavailable; SAGE requires Node >= 22.5"
67
+ );
68
+ }
69
+ const service = requireSageService(port);
70
+ const allTools = createSageTools(service);
71
+ const allowed = selectAllowedTools(allTools, opts);
72
+ const allowedByName = new Map(allowed.map((entry) => [entry.name, entry.tool]));
73
+ const ctx = createSyntheticContext();
74
+ const ac = new AbortController();
75
+ const signal = ac.signal;
76
+ function jsonSchemaToObject(schema) {
77
+ return schema;
78
+ }
79
+ return {
80
+ listTools() {
81
+ return allowed.map(
82
+ ({ name, tool }) => ({
83
+ name,
84
+ ...tool.description ? { description: tool.description } : {},
85
+ inputSchema: jsonSchemaToObject(tool.inputSchema)
86
+ })
87
+ );
88
+ },
89
+ async callTool(name, args) {
90
+ const tool = allowedByName.get(name);
91
+ if (!tool) {
92
+ return {
93
+ content: `Tool "${name}" is not exposed by this SAGE MCP server`,
94
+ isError: true
95
+ };
96
+ }
97
+ const callArgs = { ...args };
98
+ if (name === "remember" && callArgs["no_auto_audience"] === void 0) {
99
+ callArgs["no_auto_audience"] = true;
100
+ }
101
+ const validate = tool.validate;
102
+ if (typeof validate === "function") {
103
+ const errors = await validate(callArgs);
104
+ if (Array.isArray(errors) && errors.length > 0) {
105
+ return { content: errors.join("\n"), isError: true };
106
+ }
107
+ }
108
+ let output;
109
+ try {
110
+ output = await tool.execute(callArgs, ctx, { signal });
111
+ } catch (error) {
112
+ const message = error instanceof Error ? error.message : String(error);
113
+ return { content: message, isError: true };
114
+ }
115
+ if (typeof output === "string") return { content: output, isError: false };
116
+ return { content: output, isError: false };
117
+ }
118
+ };
119
+ }
120
+ function createSyntheticContext() {
121
+ return {
122
+ systemPrompt: [],
123
+ cwd: process.cwd(),
124
+ projectRoot: process.cwd(),
125
+ allowOutsideProjectRoot: false,
126
+ model: "sage-mcp",
127
+ tools: [],
128
+ meta: {}
129
+ };
130
+ }
131
+ function createSageMcpServer(port, opts = {}) {
132
+ const host = createSageMcpToolHost(port, opts);
133
+ return new MCPServer({
134
+ host,
135
+ serverInfo: { name: "wrongstack-sage-mcp", version: SERVER_INFO.version }
136
+ });
137
+ }
138
+ export {
139
+ SERVER_INFO,
140
+ createSageMcpServer,
141
+ createSageMcpToolHost,
142
+ requireSageService,
143
+ selectAllowedTools
144
+ };
145
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/adapter.ts", "../src/policy.ts", "../src/version.ts"],
4
+ "sourcesContent": ["/**\n * MCPServerToolHost adapter for SAGE Memory.\n *\n * Maps `createSageTools(port)` from `@wrongstack/sage` into the\n * transport-agnostic `MCPServerToolHost` shape expected by\n * `@wrongstack/mcp`'s `MCPServer` (`packages/mcp/src/server.ts:36-39`).\n *\n * Tool-level safety gates (e.g. `force: true` on `memory_delete` from\n * `packages/sage/src/tools/memory-tools.ts:332-340`) are preserved by passing\n * the Sage tool's `Tool.validate` + `Tool.execute` through unchanged \u2014\n * a Sage `validate` failure surfaces as `isError: true` MCP content,\n * the same end-shape `wstack mcp serve` already returns in\n * `packages/cli/src/mcp-serve.ts:298-318`.\n *\n * The synthetic `Context` mirrors `packages/cli/src/mcp-serve.ts:179-214`\n * (`makeServeContext`). It has no provider, no session, no tokenCounter.\n * SAGE's `remember` tool reads `ctx.meta['agentRole']` /\n * `ctx.meta['mode']` to auto-scope audience\n * (`packages/sage/src/tools/memory-tools.ts:166-178`); we force\n * `arguments.no_auto_audience = true` so MCP callers must opt in explicitly.\n */\nimport {\n MCPServer,\n type MCPServerCallResult,\n type MCPServerTool,\n type MCPServerToolHost,\n} from '@wrongstack/mcp';\nimport type { Context } from '@wrongstack/core/agent';\nimport type { JSONSchema, MemoryPort, Tool } from '@wrongstack/core/types';\nimport {\n type SageServiceLike,\n createSageTools,\n getSageService,\n isSqliteAvailable,\n} from '@wrongstack/sage';\nimport { selectAllowedTools, type SageMcpPolicyOptions } from './policy.js';\nimport { SERVER_INFO } from './version.js';\n\nexport interface SageMcpToolHostOptions extends SageMcpPolicyOptions {}\n\n/**\n * Resolve the SAGE service capability from any `MemoryPort`. Both\n * `SqliteMemoryPort` and `ProjectSageMemoryPort` expose it via\n * `getCapability(SAGE_SERVICE_CAPABILITY)`; if either is missing,\n * we throw a precise error.\n *\n * The capability lookup is the same shape\n * `packages/runtime/src/container.ts:131-152` already relies on for\n * wiring the IPC bound port into the tool registry.\n */\nexport function requireSageService(port: MemoryPort): SageServiceLike {\n const service = getSageService(port);\n if (!service) {\n throw new Error(\n 'SAGE MCP: the supplied MemoryPort does not expose the SAGE service capability. ' +\n 'Use the port created by createProjectSageMemoryPort(...) or SqliteMemoryPort directly.',\n );\n }\n return service;\n}\n\nexport function createSageMcpToolHost(\n port: MemoryPort,\n opts: SageMcpToolHostOptions = {},\n): MCPServerToolHost {\n if (!isSqliteAvailable()) {\n // The same gate as `packages/runtime/src/container.ts:139-144` \u2014\n // SAGE requires Node >= 22.5 with `node:sqlite`. Fail loudly here\n // instead of letting the first `Tool.execute` crash mid-protocol.\n throw new Error(\n 'SAGE MCP: node:sqlite is unavailable; SAGE requires Node >= 22.5',\n );\n }\n\n const service = requireSageService(port);\n const allTools: Tool[] = createSageTools(service);\n const allowed = selectAllowedTools(allTools, opts);\n const allowedByName = new Map<string, Tool>(allowed.map((entry) => [entry.name, entry.tool]));\n\n // Synthetic Context \u2014 no provider / session / tokenCounter. SAGE-only memory\n // ops never read those fields; `ctx.meta` is empty, which makes\n // `no_auto_audience = true` (set below) both deterministic and defensible.\n const ctx = createSyntheticContext();\n const ac = new AbortController();\n const signal = ac.signal;\n\n function jsonSchemaToObject(schema: JSONSchema): Record<string, unknown> {\n // JSONSchema's permissive `[k: string]: unknown` already satisfies the\n // MCP `Record<string, unknown>` inputSchema contract. We only narrow\n // the top-level return type for the strict MCPServerTool type.\n return schema as unknown as Record<string, unknown>;\n }\n\n return {\n listTools(): MCPServerTool[] {\n return allowed.map(\n ({ name, tool }): MCPServerTool => ({\n name,\n ...(tool.description ? { description: tool.description } : {}),\n inputSchema: jsonSchemaToObject(tool.inputSchema),\n }),\n );\n },\n\n async callTool(name: string, args: Record<string, unknown>): Promise<MCPServerCallResult> {\n const tool = allowedByName.get(name);\n if (!tool) {\n return {\n content: `Tool \"${name}\" is not exposed by this SAGE MCP server`,\n isError: true,\n };\n }\n\n // For `remember`, force no_auto_audience=true. SAGE's auto-audience\n // detection reads `ctx.meta['agentRole']`/`ctx.meta['mode']`; MCP\n // callers do not supply a role/mode so we cannot infer one.\n const callArgs: Record<string, unknown> = { ...args };\n if (name === 'remember' && callArgs['no_auto_audience'] === undefined) {\n callArgs['no_auto_audience'] = true;\n }\n\n // Tool.validate is the canonical safety gate (e.g., the `force: true`\n // requirement on memory_delete). Surface validation failures exactly\n // like `wstack mcp serve` would.\n const validate = tool.validate;\n if (typeof validate === 'function') {\n const errors = await validate(callArgs);\n if (Array.isArray(errors) && errors.length > 0) {\n return { content: errors.join('\\n'), isError: true };\n }\n }\n\n let output: unknown;\n try {\n output = await tool.execute(callArgs, ctx, { signal });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return { content: message, isError: true };\n }\n\n // The output shape mirrors how `wstack mcp serve` returns its\n // executor result (a string, see packages/cli/src/mcp-serve.ts:316).\n // Sage tools return strings, structured objects, or arrays; we let\n // `MCPServer.toContentBlocks` (server.ts:303-316) handle the\n // text-block wrapping uniformly.\n if (typeof output === 'string') return { content: output, isError: false };\n return { content: output as unknown, isError: false };\n },\n };\n}\n\nfunction createSyntheticContext(): Context {\n // Minimal Context stand-in. SAGE memory ops only read `ctx.meta`; the\n // provider/session/tokenCounter stubs throw on use, which is desired\n // because MCP-side memory calls are pure data operations.\n return {\n systemPrompt: [],\n cwd: process.cwd(),\n projectRoot: process.cwd(),\n allowOutsideProjectRoot: false,\n model: 'sage-mcp',\n tools: [],\n meta: {},\n } as unknown as Context;\n}\n\nexport function createSageMcpServer(\n port: MemoryPort,\n opts: SageMcpToolHostOptions = {},\n): MCPServer {\n const host = createSageMcpToolHost(port, opts);\n return new MCPServer({\n host,\n serverInfo: { name: 'wrongstack-sage-mcp', version: SERVER_INFO.version },\n });\n}\n", "/**\n * Tool allowlist policy for the SAGE MCP server.\n *\n * Mirrors `packages/cli/src/mcp-serve.ts:217-236` (selectExposedTools).\n *\n * Tool permission model:\n * - Sage tools use `permission: 'confirm'` for any write/delete/update/\n * recover/backfill/verify/hygiene operation\n * (`packages/sage/src/tools/memory-tools.ts`), reserving\n * `permission: 'auto'` for read-only tools.\n * - `wstack mcp serve` (`packages/cli/src/mcp-serve.ts:217-236`)\n * decides auto-approval by routing through a host `PermissionPolicy`\n * (`AutoApprovePermissionPolicy` defaults to read-only).\n *\n * SAGE MCP chooses a simpler axis: MCP has no UI confirm flow, so a write\n * tool surfaced over MCP cannot be approved inline \u2014 the MCP client (e.g.\n * Claude Desktop) is expected to surface its own confirm UX before\n * forwarding the call. Default policy is therefore \"read-only\", exposing\n * only `permission: 'auto'` AND `riskTier === 'safe'` tools. Pass\n * `--writable` to additionally expose `permission === 'confirm'` tools\n * whose `riskTier !== 'destructive'`. The MCP client owns the user-facing\n * confirmation gesture in that case.\n *\n * The tool's own `Tool.validate(input)` (see\n * `packages/sage/src/tools/memory-tools.ts:276-283` and `:332-340`) remains\n * the source of truth for per-call safety checks such as `force: true` on\n * `memory_delete`. Policy only controls visibility.\n */\nimport type { Tool } from '@wrongstack/core/types';\n\nexport interface SageMcpPolicyOptions {\n writable?: boolean;\n}\n\nexport interface SageMcpAllowedTool {\n name: string;\n tool: Tool;\n}\n\nexport function selectAllowedTools(\n tools: Tool[],\n opts: SageMcpPolicyOptions = {},\n): SageMcpAllowedTool[] {\n const allowed: SageMcpAllowedTool[] = [];\n for (const tool of tools) {\n if (tool.permission === 'deny') continue;\n if (tool.riskTier === 'destructive') continue;\n if (tool.permission === 'auto') {\n if (tool.riskTier === 'safe' || opts.writable === true) {\n allowed.push({ name: tool.name, tool });\n }\n continue;\n }\n // permission === 'confirm'\n if (opts.writable !== true) continue;\n if (tool.riskTier !== 'standard' && tool.riskTier !== 'safe') continue;\n allowed.push({ name: tool.name, tool });\n }\n return allowed;\n}\n", "/**\n * Single source of truth for the package version reported to MCP clients.\n * Kept tiny so a build step never has to run before `pnpm --filter` tests\n * can read it.\n */\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, resolve } from 'node:path';\n\nconst here = dirname(fileURLToPath(import.meta.url));\nconst pkgPath = resolve(here, '..', 'package.json');\n\ninterface MinimalPackage {\n name?: string;\n version?: string;\n}\n\nlet cached: { name: string; version: string } | undefined;\nfunction readServerInfo(): { name: string; version: string } {\n if (cached) return cached;\n try {\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as MinimalPackage;\n cached = {\n name: pkg.name ?? '@wrongstack/sage-mcp',\n version: pkg.version ?? '0.0.0',\n };\n return cached;\n } catch {\n cached = { name: '@wrongstack/sage-mcp', version: '0.0.0' };\n return cached;\n }\n}\n\nexport const SERVER_INFO = readServerInfo();\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Tool allowlist policy for the SAGE MCP server.
3
+ *
4
+ * Mirrors `packages/cli/src/mcp-serve.ts:217-236` (selectExposedTools).
5
+ *
6
+ * Tool permission model:
7
+ * - Sage tools use `permission: 'confirm'` for any write/delete/update/
8
+ * recover/backfill/verify/hygiene operation
9
+ * (`packages/sage/src/tools/memory-tools.ts`), reserving
10
+ * `permission: 'auto'` for read-only tools.
11
+ * - `wstack mcp serve` (`packages/cli/src/mcp-serve.ts:217-236`)
12
+ * decides auto-approval by routing through a host `PermissionPolicy`
13
+ * (`AutoApprovePermissionPolicy` defaults to read-only).
14
+ *
15
+ * SAGE MCP chooses a simpler axis: MCP has no UI confirm flow, so a write
16
+ * tool surfaced over MCP cannot be approved inline — the MCP client (e.g.
17
+ * Claude Desktop) is expected to surface its own confirm UX before
18
+ * forwarding the call. Default policy is therefore "read-only", exposing
19
+ * only `permission: 'auto'` AND `riskTier === 'safe'` tools. Pass
20
+ * `--writable` to additionally expose `permission === 'confirm'` tools
21
+ * whose `riskTier !== 'destructive'`. The MCP client owns the user-facing
22
+ * confirmation gesture in that case.
23
+ *
24
+ * The tool's own `Tool.validate(input)` (see
25
+ * `packages/sage/src/tools/memory-tools.ts:276-283` and `:332-340`) remains
26
+ * the source of truth for per-call safety checks such as `force: true` on
27
+ * `memory_delete`. Policy only controls visibility.
28
+ */
29
+ import type { Tool } from '@wrongstack/core/types';
30
+ export interface SageMcpPolicyOptions {
31
+ writable?: boolean;
32
+ }
33
+ export interface SageMcpAllowedTool {
34
+ name: string;
35
+ tool: Tool;
36
+ }
37
+ export declare function selectAllowedTools(tools: Tool[], opts?: SageMcpPolicyOptions): SageMcpAllowedTool[];
38
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,IAAI,EAAE,EACb,IAAI,GAAE,oBAAyB,GAC9B,kBAAkB,EAAE,CAiBtB"}
@@ -0,0 +1,5 @@
1
+ export declare const SERVER_INFO: {
2
+ name: string;
3
+ version: string;
4
+ };
5
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,WAAW;UAfW,MAAM;aAAW,MAAM;CAef,CAAC"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@wrongstack/sage-mcp",
3
+ "version": "0.296.2",
4
+ "license": "MIT",
5
+ "description": "WrongStack SAGE Memory as an MCP (Model Context Protocol) server: STDIO and loopback HTTP, additive to the existing SAGE IPC server.",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/WrongStack/WrongStack.git",
9
+ "directory": "packages/sage-mcp"
10
+ },
11
+ "homepage": "https://github.com/WrongStack/WrongStack#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/WrongStack/WrongStack/issues"
14
+ },
15
+ "author": "ECOSTACK TECHNOLOGY OÜ",
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "bin": {
20
+ "wstack-sage-mcp": "./dist/cli.js"
21
+ },
22
+ "sideEffects": false,
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "dependencies": {
33
+ "@wrongstack/core": "0.296.2",
34
+ "@wrongstack/sage": "0.296.2",
35
+ "@wrongstack/mcp": "0.296.2"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^26.1.1",
39
+ "typescript": "^7.0.2",
40
+ "vitest": "^4.1.10"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "node ../../scripts/build-package.mjs",
47
+ "typecheck": "tsc --noEmit -p tsconfig.test.json",
48
+ "test": "echo \"Run @wrongstack/sage-mcp tests from the workspace root: pnpm exec vitest run packages/sage-mcp/tests\"",
49
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
50
+ }
51
+ }