Mnemonica Strategy
# @mnemonica/strategy
MCP (Model Context Protocol) server that lets an AI agent **work on a running
Mnemonica runtime** — inspect its type tree, define new types, construct
instances, and swap constructor handlers in flight — without stopping the
server you are developing.
## Overview
Strategy is the live bridge between a running Mnemonica runtime and the
tools around it. It attaches to a target Node.js process via the Chrome
Debug Protocol (CDP) — zero instrumentation of the target — and then moves
the real work onto a fast WebSocket construction channel injected into the
process. It is the central point of the topology: agents drive it over MCP;
[Mnemographica](https://github.com/mythographica/mnemographica) connects as
the monitoring client — through Strategy's own trace channel, or DIRECTLY to
an app's self-hosted channel (`startStrategyClient` + `traceSubscribe`, the
App Channel tab — no CDP in between).
The development loop this enables — the main server **never stops**:
1. Your app runs as usual (a debug-enabled child copy via
[infer-debug](https://github.com/wentout/infer-debug) works too).
2. Strategy attaches over CDP — once. CDP is the delivery truck, not the road.
3. A dependency-free WebSocket server is injected into the runtime; all
construction traffic (`define` / `instantiate` / `swap`) moves there.
4. New types are **born shimmed**: their constructor is a stable shell whose
handler lives in the session's closure, so `ws_swap` can replace the
implementation in flight — existing constructors and instances are
untouched.
5. When a shape is proven, [Tactica](https://www.npmjs.com/package/@mnemonica/tactica)
crystallizes it into `.tactica` type definitions.
Strategy can also compare the runtime type tree against Tactica-generated
types to validate static analysis — its original purpose, still available.
## Installation
```bash
npm install @mnemonica/strategy
```
From source instead:
```bash
git clone https://github.com/mythographica/strategy.git
cd strategy
npm install
npm run build
```
## Usage
### Prerequisites
Your target application must be running with the debug flag:
```bash
# For NestJS
nest start --debug --watch
# For regular Node.js
node --inspect=9229 your-app.js
```
Don't want `--inspect` on the main process? [infer-debug](https://github.com/wentout/infer-debug)
can spawn a debug-enabled child copy of the app on demand and tunnel CDP
through the app's own HTTP port — strategy attaches to that child the same
way.
### As MCP Server
```bash
# installed from npm
npx @mnemonica/strategy
# from a source checkout
node /path/to/strategy/lib/cli.js
```
### MCP Configuration
Add to your agent framework's MCP config:
```json
{
"mcpServers": {
"mnemonica-strategy": {
"command": "npx",
"args": ["-y", "@mnemonica/strategy"]
}
}
}
```
or, from a source checkout:
```json
{
"mcpServers": {
"mnemonica-strategy": {
"command": "node",
"args": ["/path/to/strategy/lib/cli.js"]
}
}
}
```
## MCP Tools Provided
The Strategy MCP server exposes **3 bundled tools**:
### 1. `execute`
Execute any command from the 3 context folders (MCP, RPC, RUN).
**Input:**
- `context` (string, required): Execution context - "MCP", "RPC", or "RUN"
- `command` (string, required): Command name to execute
- `message` (string, optional): JSON string containing command arguments
**Example:**
```javascript
// Connect to Node.js debugger
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"connect\", \"host\": \"localhost\", \"port\": 9229 }"
}
// Check connection status
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"status\" }"
}
// Analyze the runtime type hierarchy
execute {
context: "RPC",
command: "rpc_analyze_type_hierarchy",
message: "{}"
}
```
### 2. `list`
List available commands by context.
**Input:**
- `context` (string, required): "MCP", "RPC", "RUN", or "ALL"
**Example:**
```javascript
list { context: "ALL" }
```
### 3. `help`
Get detailed help for any command.
**Input:**
- `context` (string, required): Command context
- `command` (string, required): Command name
**Example:**
```javascript
help { context: "RPC", command: "rpc_connection" }
```
## Args Passing Mechanism (IMPORTANT)
Due to MCP protocol limitations, command arguments must be passed as a **JSON string** in the `message` field, not as direct object properties.
**Correct format:**
```javascript
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"connect\", \"host\": \"localhost\", \"port\": 9229 }"
}
```
**Incorrect format (will not work):**
```javascript
// DON'T DO THIS
execute {
context: "RPC",
command: "rpc_connection",
args: { action: "connect" } // This won't work!
}
```
## Common Commands
### Connection Management
```javascript
// Connect to Node.js debugger
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"connect\", \"host\": \"localhost\", \"port\": 9229 }"
}
// Check connection status
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"status\" }"
}
// Disconnect from runtime
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"disconnect\" }"
}
```
### Type Analysis
```javascript
// Analyze the complete type hierarchy (recursive subtype tree from the target)
execute {
context: "RPC",
command: "rpc_analyze_type_hierarchy",
message: "{}"
}
// Create type in the target runtime via CDP
execute {
context: "RPC",
command: "rpc_create_type",
message: "{ \"typeName\": \"MyType\" }"
}
// Load Tactica-generated types
execute {
context: "MCP",
command: "mcp_load_remote_tactica_types",
message: "{ \"projectPath\": \"/path/to/project\" }"
}
// Compare runtime vs Tactica types
execute {
context: "MCP",
command: "mcp_compare_with_tactica",
message: "{ \"projectPath\": \"/path/to/project\" }"
}
```
## Live development on a running app
Strategy can change a running app **in flight** — no restart, no
redeploy. Two modes share one mechanism: the app (or its debug child)
self-hosts the channel, and every change reaches the type registry or the
module system behind the same constructor and module identities the app
already holds.
### The channel as a path on the app's own server
`startStrategyClient({ server, path, role, attach? })` mounts the channel
on an EXISTING `http.Server` as a WebSocket upgrade path (default
`/strategy`) — no separate port. Two roles split the surface:
| role | serves | refuses |
|---|---|---|
| `observer` (the main process) | traces (`traceSubscribe`), reads (`ping`, `list`, `patched`) | every write op — `patch`, `rollback`, `reload`, `liveEdit`, `eval`, `swap`, `define`, `instantiate` — with a readable error |
| `debug` (the secondary) | the hot-swap tools: `patch`, `rollback`, `patched`, `reload`, `liveEdit`, `eval` | trace ops |
Omit `role` for the full surface (development default). `attach: false`
skips self-attaching and returns `handle.upgradeHandler` for the app to
hand to its own upgrade router (see infer-debug below). With no options at
all the channel keeps its standalone listener for apps without a router.
### Usage with infer-debug
With [infer-debug](https://github.com/wentout/infer-debug), one pod
exposes one port, and infer-debug is the single upgrade decision point on
the app's server:
```javascript
const core = new InferDebugCore({
childPortEnvVar : 'APP_PORT',
wsRelay : ['/strategy'],
appUpgradeHandler : (req, socket) => strategyUpgrade(req, socket),
});
// …
const channel = await startStrategyClient({
server : httpServer,
path : '/strategy',
role : isMain ? 'observer' : 'debug',
attach : false,
});
strategyUpgrade = channel.upgradeHandler;
core.attachServer(httpServer);
```
Routing: the uuid-shaped inspector path tunnels to the secondary's
inspector (DevTools); an upgrade carrying the `infer-debug` header on a
`wsRelay` path is relayed wholesale to the secondary (header consumed); an
upgrade carrying the header on any other path is served by the main app
with one log line; unmarked upgrades go to the main app. Marked relay
upgrades get a clean `503` while the secondary is down. The agent's client
sends the mark on its own connection:
```javascript
const session = await WSSession.connect(host, port, token, '/strategy', {
'infer-debug' : '1',
});
```
### Mode 1 — live patch session (mnemonica types)
`patch {path, body}` replaces the construct handler of an EXISTING type
in place: the constructor identity never changes — captured references and
previously built instances keep working, the next construction runs the
new handler. `rollback {path}` pops one step (two patches, one rollback →
the first patch is active again); `rollback {path, all:true}` or
`{all:true}` restores the original exactly; `patched` lists paths with
stack depth and sources. Every patch gets an automatic, unique
`//# sourceURL=strategy-patch/<Type>@<n>.js` so DevTools lists and
searches each version separately. Unmarked requests never see a patch; the
released behaviour is always one rollback away.
### Mode 2 — module reload with compiled JS
Only JS runs: the app is its built package, compiled with
`inlineSourceMap` + `inlineSources` so every built file carries its own
map. The agent edits the TypeScript source LOCALLY, compiles that one
module with the app's `tsconfig`, and sends the result over the channel.
`reload {module, code}` — primary mechanism:
- The code is evaluated under the module's real filename with a `?v=N`
suffix, so DevTools lists each reload separately and the embedded map
still shows the TS.
- During that evaluation `define` is intercepted: types the file declares
again keep the SAME type object with only the handler swapped (instances,
`lookup()`, subtypes stay connected); new types define normally; a type
removed from the file stays declared (types cannot be undeclared).
- Hooks are journaled per module: exactly what the file registered before
is removed, what it registers now is kept — two hooks of the same type
both stay, other modules' hooks on the same type survive. Two limits:
the FIRST reload of a module attributes its original hooks **by
source-text match** against the original script (the original load was
never watched; when the debugger cannot surface the source, originals
linger instead of risking foreign removals — the result says
`hookSeeded:false`); hooks a module adds to types it does NOT define are
not tracked.
- Handler swaps are staged and applied only after the module evaluates
cleanly: a file that throws leaves everything as it was and consumes no
version. The ENTRY module (the process entry point) is refused — that is
a full restart. CommonJS only; an ESM module fails with a readable
compilation error.
- The old `module.exports` object is then replaced IN PLACE: in-flight
requests finish on the old code, the next request runs the new one.
Held references: call sites that read through the exports object (all
tsc-compiled imports) see the new code; true destructured
`const { fn } = require(...)` bindings keep the old function — that is
what `liveEdit` is for.
`liveEdit {module, code}` — explicit fallback: `Debugger.setScriptSource`
edits the running script in place and reaches even those captured
bindings. V8's rules apply: the edit must keep the script's structural
positions (a one-function body or signature change — structural rewrites
are `reload`'s job), and V8 **refuses while a request is suspended inside
the old function**: the status (`BlockedByActiveGenerator`) is returned
verbatim — wait for the request to finish and retry. After a reload, the
newest `?v=N` script is targeted.
### The agent's workflow (target scenario)
1. Watch the failing request in the secondary (marked requests, traces).
2. At the failing code, read the scope — the REAL third-party data.
3. Edit the source locally, run `tsc`, deliver through the channel:
mnemonica types go through `patch` — or `reload` when the type lives in
a reloaded module; plain code goes through `reload`; `liveEdit` only
when stale destructured bindings must be reached. No redeployment.
4. Replay the request until the reply is right.
5. Report "debugged — commit this" with a regression test built from the
payload captured at step 2; or, after repeated rounds, report why not
and what the scope showed.
**Data note.** This workflow exposes production data: scopes, traces and
captured payloads are real user input. Who may open a session and where
captured data may end up is a policy decision for the team running the
app; treat every capture as sensitive.
## Example Workflow
1. Start your Mnemonica application with debug mode:
```bash
# any Mnemonica app, e.g. a NestJS service
nest start --debug --watch
# or plain Node.js
node --inspect=9229 your-app.js
```
2. Connect to the debugger:
```javascript
execute {
context: "RPC",
command: "rpc_connection",
message: "{ \"action\": \"connect\" }"
}
```
3. Analyze runtime types:
```javascript
execute {
context: "RPC",
command: "rpc_analyze_type_hierarchy",
message: "{}"
}
```
4. Compare with Tactica-generated types:
```javascript
execute {
context: "MCP",
command: "mcp_compare_with_tactica",
message: "{ \"projectPath\": \"/path/to/project\" }"
}
```
## The construction channel (`ws_` commands)
After `rpc_connection` is up, one call injects the WS channel into the
target; everything after that is fast WS traffic, not CDP:
```javascript
// 1. Bootstrap: inject the WS server into the target (one CDP evaluate)
execute { context: "RPC", command: "ws_bootstrap", message: "{}" }
// 2. Define a type — born shimmed (swappable later)
execute {
context: "MCP",
command: "ws_define",
message: "{ \"name\": \"TempProbe\", \"body\": \"function (data) { this.value = data.value; }\" }"
}
// 3. Construct an instance of it, right now, in the running process
execute {
context: "MCP",
command: "ws_instantiate",
message: "{ \"path\": \"TempProbe\", \"args\": [{ \"value\": 42 }] }"
}
// → { chain: ["Mnemonica", "TempProbe"], props: { value: 42 } }
// 4. Swap the handler in flight — the constructor identity never changes
execute {
context: "MCP",
command: "ws_swap",
message: "{ \"path\": \"TempProbe\", \"body\": \"function (data) { this.value = data.value * 2; }\" }"
}
// 5. Next instance uses the NEW implementation
execute {
context: "MCP",
command: "ws_instantiate",
message: "{ \"path\": \"TempProbe\", \"args\": [{ \"value\": 42 }] }"
}
// → { props: { value: 84 } }
// Session state: which types are shimmed/swappable
execute { context: "MCP", command: "ws_session", message: "{ \"action\": \"list\" }" }
```
Rules the channel enforces (they are mnemonica semantics, not policy):
- `ws_swap` refuses any type not born via `ws_define` in this session —
pre-existing types are never re-defined.
- Subtypes construct from parent **instances**: for nested paths,
`ws_instantiate` walks the chain, taking intermediate constructor args
from `chainArgs` (e.g. `{ "TempProbe": [{ "value": 7 }] }`).
- Async constructor handlers must `return this` (mnemonica enforces this).
The in-target server is development-only instrumentation: it binds
`127.0.0.1`, requires a per-session token at the WebSocket handshake, and
disappears with the process. Do not expose it on production runtimes.
## Command Contexts
| Context | Folder | Execution Environment |
|---------|--------|----------------------|
| MCP | `commands-mcp/` | Local MCP server process |
| RPC | `commands-rpc/` | Local orchestration; effects in the target via CDP |
| RUN | `commands-run/` | Local side effects (files, utilities) |
Command names carry their site as a prefix (`mcp_`, `rpc_`, `run_`), so
the place of execution is visible in the name itself. The `ws_` prefix
marks the construction channel: `ws_bootstrap` lives in `commands-rpc/`
(it needs CDP to get in), every other `ws_*` command lives in
`commands-mcp/` and talks to the stored WS session.
## How strategy's own types are preserved
Strategy's live state (runtime, command contexts, connections, channels)
is typed as mnemonica types in `src/strategy-types.ts`, using **builder
mode on the default collection**: a `mnemonica.define(...)` chain whose
value carries a LOCAL registry derived from the handlers. The exported
API is the builder value (`StrategyTypes`) plus the looked-up
constructors (`StrategyTypes.lookup('StrategyRuntime')` and the
children) — never the raw `define()` results — so declaration emit
stays portable on TypeScript 6 (mnemonica >= 1.3.6; see mnemonica
`docs/typed-lookup.md`, "Declaration emit on TypeScript 6").
| | hand-written `TypeRegistry` merge | builder mode (this repo) |
|---|---|---|
| registry scope | global (module augmentation) | local, carried by the exported value |
| derived from | hand-written declarations | the handlers in this file |
| drift | can drift from the handlers | cannot drift |
| code change | minimal (one merge block) | chain value + lookups |
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run watch
# Test
npm run test
```
## CDP Scripts Architecture
The `cdp-scripts/` folder contains scripts that execute inside the target Node.js runtime via Chrome Debug Protocol:
```
cdp-scripts/
├── create-type.js # Creates mnemonica types in the target
├── analyze-hierarchy.js # Retrieves complete type hierarchy
└── ws-server.js # Phase 3: the injected WS construction server
```
**How it works:**
1. MCP command reads the script file
2. (create-type only) injects `var args = {...}` at the top with command arguments
3. Sends it to the target via `client.Runtime.evaluate({ expression: script, awaitPromise: true })`
4. Script executes inside the target process
5. Return value is sent back to the MCP process
**Key pattern — the canonical prelude.** Scripts must never use a bare
`require` (there is none in evaluated code) and never rely on
`process.mainModule.require` alone (it is `undefined` in ESM-entry
processes, and `import()` in evaluated code throws
`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`). Every cdp-script loads the
target's own mnemonica through this exact three-tier prelude:
```javascript
var mnemonica;
if (process.mainModule && process.mainModule.require) {
mnemonica = process.mainModule.require('mnemonica');
} else if (typeof process.getBuiltinModule === 'function') {
var nodeModule = process.getBuiltinModule('node:module');
var cwdRequire = nodeModule.createRequire(process.cwd() + '/__strategy_cwd__.js');
mnemonica = cwdRequire('mnemonica');
} else {
var mnemonicaNs = await import('mnemonica');
mnemonica = mnemonicaNs.default || mnemonicaNs;
}
```
Scripts using it must be async IIFEs. `ws-server.js` generalizes the same
three tiers into a `targetRequire` factory because it also needs
`node:http`/`node:crypto`.
```javascript
// Access types via the defaultCollection Map (avoids proxy enumeration issues)
mnemonica.defaultCollection.forEach(function (Type, name) {
// Process each type
});
// Recursive traversal for subtype hierarchy
function getSubtypes (Type) {
var subtypes = [];
Type.subtypes.forEach(function (SubType, name) {
subtypes.push({
name: name,
subtypes: getSubtypes(SubType) // Recursive
});
});
return subtypes;
}
```
## Creating Commands
Commands are JavaScript files in the `commands-*/` folders with MCP Tool Metadata.
Two shapes are supported; **`module.exports.run` is the pattern current
commands use**:
```javascript
/**
* MCP Tool Metadata:
* {
* "name": "mcp_my_command",
* "description": "What this command does",
* "inputSchema": {
* "type": "object",
* "properties": {
* "argName": { "type": "string" }
* }
* }
* }
*/
async function run (ctx) {
const { require, args, store } = ctx;
// Parse message if present (args arrive as a JSON string in `message`)
let commandArgs = args;
if (args.message && typeof args.message === 'string') {
try {
commandArgs = JSON.parse(args.message);
} catch (e) {
return { success: false, error: 'Invalid JSON: ' + e.message };
}
}
return { success: true, data: { got: commandArgs.argName } };
}
module.exports = { run };
```
Files without a `run` export are instead wrapped in an async IIFE with
`ctx` in scope. Two gotchas, both learned the hard way:
- **Never `ctx.require('mnemonica')`** — that resolves in the MCP process,
not the target. Target-side mnemonica work belongs in `cdp-scripts/` with
the canonical prelude.
- **`ctx.require` resolves relative paths from `lib/server.js`**, not from
your command file — require lib modules by absolute path
(`path.join(__dirname, '../../lib/...')`), the same idiom used for
`cdp-scripts/`.
## License
MIT
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: execute performs actions, help provides documentation, and list enumerates available commands. There is no overlap in functionality, making it easy for an agent to select the correct tool without confusion.
All tool names follow a consistent, simple verb-only pattern (execute, help, list). This uniformity makes the set predictable and easy to understand, with no deviations in naming style.
With only 3 tools, the set feels thin for a strategy server, potentially lacking depth in operational coverage. While the tools are well-defined, the low count may limit the server's utility in complex scenarios, though it's not extreme.
The tool surface is significantly incomplete for a strategy domain; it only covers command execution, help, and listing, with no tools for planning, analysis, or decision-making. This creates obvious gaps that could lead to agent failures in strategic tasks.