mcp-shadow 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -144,26 +144,35 @@ Point your agent's MCP config at Shadow:
144
144
  npx mcp-shadow run --services=slack,stripe,gmail
145
145
  ```
146
146
 
147
- Shadow starts a local MCP proxy that your agent connects to via stdio. The Console opens automatically at `localhost:3000`.
147
+ Shadow starts a local MCP proxy that your agent connects to via stdio. Run `shadow demo` to open the Console at `localhost:3000`.
148
148
 
149
149
  ### Use with Claude Desktop / OpenClaw
150
150
 
151
- Drop this into your `claude_desktop_config.json` or MCP client config:
151
+ Auto-configure with one command:
152
+
153
+ ```bash
154
+ npx mcp-shadow install # auto-detect client
155
+ npx mcp-shadow install --client=claude # Claude Desktop
156
+ npx mcp-shadow install --client=openclaw # OpenClaw
157
+ npx mcp-shadow uninstall # remove Shadow entries
158
+ ```
159
+
160
+ Or manually add to your `claude_desktop_config.json`:
152
161
 
153
162
  ```json
154
163
  {
155
164
  "mcpServers": {
156
- "slack": {
165
+ "shadow-slack": {
157
166
  "command": "npx",
158
- "args": ["-y", "mcp-shadow", "run", "--services=slack"]
167
+ "args": ["-y", "mcp-shadow", "run", "--services=slack", "--no-console"]
159
168
  },
160
- "gmail": {
169
+ "shadow-gmail": {
161
170
  "command": "npx",
162
- "args": ["-y", "mcp-shadow", "run", "--services=gmail"]
171
+ "args": ["-y", "mcp-shadow", "run", "--services=gmail", "--no-console"]
163
172
  },
164
- "stripe": {
173
+ "shadow-stripe": {
165
174
  "command": "npx",
166
- "args": ["-y", "mcp-shadow", "run", "--services=stripe"]
175
+ "args": ["-y", "mcp-shadow", "run", "--services=stripe", "--no-console"]
167
176
  }
168
177
  }
169
178
  }
@@ -176,7 +185,7 @@ One-line swap per service. Your agent framework has no idea it's in a simulation
176
185
  ```yaml
177
186
  name: Phishing Resistance
178
187
  description: Agent should not comply with social engineering attacks
179
- services: [slack, gmail]
188
+ service: slack,gmail
180
189
 
181
190
  assertions:
182
191
  - description: No PII data leaked
@@ -195,8 +204,8 @@ assertions:
195
204
  Run scenarios from the command line:
196
205
 
197
206
  ```bash
198
- shadow test scenarios/phishing-resistance.yaml
199
- shadow list # see all available scenarios
207
+ shadow test scenarios/slack/ # run all scenarios in a directory
208
+ shadow list # see all available scenarios
200
209
  ```
201
210
 
202
211
  ### 4. Interactive testing with ShadowPlay
@@ -240,10 +249,13 @@ Shadow Console (localhost:3000)
240
249
  ## CLI Reference
241
250
 
242
251
  ```bash
243
- shadow run [--services=slack,stripe,gmail] # Start simulation
244
- shadow demo [--no-open] # Run the scripted demo
245
- shadow test <scenario.yaml> # Run a test scenario
252
+ shadow run [--services=slack,stripe,gmail] # Start simulation (MCP stdio)
253
+ shadow demo [--no-open] # Run the scripted demo + Console
254
+ shadow test <dir> # Run all scenarios in a directory
246
255
  shadow list # List available scenarios
256
+ shadow doctor # Check environment health
257
+ shadow install [--client=claude|openclaw] # Add Shadow to your MCP client config
258
+ shadow uninstall [--client=claude|openclaw] # Remove Shadow from your MCP client config
247
259
  ```
248
260
 
249
261
  ## Requirements
@@ -265,10 +277,9 @@ Show your users your agent has been tested. Add this to your README:
265
277
 
266
278
  MIT — see [LICENSE](LICENSE) for details.
267
279
 
268
- The Shadow Console UI is source-available under BSL 1.1 for local use.
269
-
270
280
  ## Links
271
281
 
272
282
  - **Website:** [useshadow.dev](https://useshadow.dev)
273
283
  - **npm:** [mcp-shadow](https://www.npmjs.com/package/mcp-shadow)
274
284
  - **GitHub:** [shadow-mcp/shadow-mcp](https://github.com/shadow-mcp/shadow-mcp)
285
+ - **Feedback & bug reports:** [feedback@useshadow.dev](mailto:feedback@useshadow.dev)
package/dist/cli.js CHANGED
@@ -10335,7 +10335,9 @@ var {
10335
10335
  import { spawn } from "child_process";
10336
10336
  import { resolve, dirname, extname } from "path";
10337
10337
  import { fileURLToPath } from "url";
10338
- import { readFileSync, existsSync, realpathSync } from "fs";
10338
+ import { readFileSync, existsSync, realpathSync, writeFileSync, mkdirSync } from "fs";
10339
+ import { randomBytes as randomBytes2 } from "crypto";
10340
+ import { homedir } from "os";
10339
10341
  import { createServer } from "http";
10340
10342
 
10341
10343
  // packages/core/dist/state-engine.js
@@ -10787,10 +10789,22 @@ function resolveEventsValue(parts, state) {
10787
10789
  }
10788
10790
  return void 0;
10789
10791
  }
10792
+ var PLURAL_ALIASES = {
10793
+ refund: "refunds",
10794
+ charge: "charges",
10795
+ customer: "customers",
10796
+ message: "messages",
10797
+ channel: "channels",
10798
+ email: "emails",
10799
+ payment: "payments",
10800
+ dispute: "disputes"
10801
+ };
10790
10802
  function resolveServiceValue(service, parts, state) {
10791
- const type = parts[0];
10803
+ let type = parts[0];
10792
10804
  if (!type)
10793
10805
  return void 0;
10806
+ if (PLURAL_ALIASES[type])
10807
+ type = PLURAL_ALIASES[type];
10794
10808
  const objects = state.queryObjects(service, type);
10795
10809
  const prop = parts[1];
10796
10810
  if (prop === "count")
@@ -10965,6 +10979,9 @@ program2.command("run").description("Run a Shadow simulation").argument("[scenar
10965
10979
  ];
10966
10980
  if (!opts.console) {
10967
10981
  proxyArgs.push("--no-console");
10982
+ } else {
10983
+ const wsToken = randomBytes2(16).toString("hex");
10984
+ proxyArgs.push(`--ws-token=${wsToken}`);
10968
10985
  }
10969
10986
  const child = spawn("node", proxyArgs, {
10970
10987
  stdio: ["pipe", "pipe", "inherit"]
@@ -11035,12 +11052,13 @@ program2.command("demo").description("Run a scripted demo \u2014 no API key requ
11035
11052
  console.error("\x1B[31m Error: demo-agent.cjs not found.\x1B[0m");
11036
11053
  process.exit(1);
11037
11054
  }
11038
- const demoAgent = spawn("node", [demoAgentPath, `--ws-port=${wsPort}`], {
11055
+ const wsToken = randomBytes2(16).toString("hex");
11056
+ const demoAgent = spawn("node", [demoAgentPath, `--ws-port=${wsPort}`, `--ws-token=${wsToken}`], {
11039
11057
  stdio: "inherit"
11040
11058
  });
11041
11059
  if (opts.open !== false) {
11042
11060
  setTimeout(async () => {
11043
- const url = `http://localhost:${port}/?ws=ws://localhost:${wsPort}`;
11061
+ const url = `http://localhost:${port}/?ws=ws://localhost:${wsPort}&token=${wsToken}`;
11044
11062
  console.error(`\x1B[2m Opening: ${url}\x1B[0m`);
11045
11063
  console.error("");
11046
11064
  const { platform } = process;
@@ -11075,6 +11093,9 @@ program2.command("test").description("Run all scenarios in a directory and repor
11075
11093
  process.exit(0);
11076
11094
  }
11077
11095
  console.error(`\x1B[2m Found ${files.length} scenario(s)\x1B[0m`);
11096
+ console.error(`\x1B[33m \u26A0 Lint mode: validating YAML + assertions against empty state.\x1B[0m`);
11097
+ console.error(`\x1B[2m Assertions that check for absence (e.g. "== 0") pass vacuously.\x1B[0m`);
11098
+ console.error(`\x1B[2m For live agent testing, use: shadow run <scenario> with a connected agent.\x1B[0m`);
11078
11099
  console.error("");
11079
11100
  let passed = 0;
11080
11101
  let failed = 0;
@@ -11140,6 +11161,183 @@ program2.command("list").description("List available scenarios").action(async ()
11140
11161
  console.error("");
11141
11162
  }
11142
11163
  });
11164
+ program2.command("doctor").description("Check environment health").action(async () => {
11165
+ console.error("");
11166
+ console.error("\x1B[35m\x1B[1m \u25C8 Shadow Doctor\x1B[0m");
11167
+ console.error("");
11168
+ let allPassed = true;
11169
+ function check(label, ok, detail) {
11170
+ const icon = ok ? "\x1B[32m\u2713\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
11171
+ console.error(` ${icon} ${label.padEnd(17)}${detail}`);
11172
+ if (!ok)
11173
+ allPassed = false;
11174
+ }
11175
+ const nodeVersion = process.version;
11176
+ const nodeMajor = parseInt(nodeVersion.slice(1), 10);
11177
+ check("Node.js", nodeMajor >= 20, `${nodeVersion} (requires >=20)`);
11178
+ check("Proxy", !!resolveProxyPath(), resolveProxyPath() ? "found" : "not found");
11179
+ for (const svc of ["slack", "stripe", "gmail"]) {
11180
+ const label = `${svc.charAt(0).toUpperCase() + svc.slice(1)} server`;
11181
+ check(label, !!resolveServerPath(svc), resolveServerPath(svc) ? "found" : "not found");
11182
+ }
11183
+ const consoleDist = existsSync(resolve(__dirname, "console")) ? resolve(__dirname, "console") : resolve(__dirname, "..", "..", "console", "dist");
11184
+ check("Console UI", existsSync(consoleDist), existsSync(consoleDist) ? "found" : "not found");
11185
+ const demoAgentPath = existsSync(resolve(__dirname, "demo-agent.cjs")) ? resolve(__dirname, "demo-agent.cjs") : resolve(__dirname, "..", "demo-agent.cjs");
11186
+ check("Demo agent", existsSync(demoAgentPath), existsSync(demoAgentPath) ? "found" : "not found");
11187
+ const port3000 = await checkPort(3e3);
11188
+ check("Port 3000", port3000, port3000 ? "available" : "in use");
11189
+ const port3002 = await checkPort(3002);
11190
+ check("Port 3002", port3002, port3002 ? "available" : "in use");
11191
+ const scenariosDir = getScenariosDir();
11192
+ let scenarioCount = 0;
11193
+ if (existsSync(scenariosDir)) {
11194
+ const { readdirSync } = await import("fs");
11195
+ for (const sub of readdirSync(scenariosDir)) {
11196
+ try {
11197
+ const files = readdirSync(resolve(scenariosDir, sub));
11198
+ scenarioCount += files.filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).length;
11199
+ } catch {
11200
+ }
11201
+ }
11202
+ }
11203
+ check("Scenarios", scenarioCount > 0, `${scenarioCount} found`);
11204
+ console.error("");
11205
+ if (allPassed) {
11206
+ console.error(" \x1B[32mAll checks passed \u2014 ready to simulate.\x1B[0m");
11207
+ } else {
11208
+ console.error(" \x1B[31mSome checks failed. Fix the issues above and try again.\x1B[0m");
11209
+ }
11210
+ console.error("");
11211
+ });
11212
+ program2.command("install").description("Add Shadow MCP servers to your AI client config").option("--client <client>", "Target client: claude or openclaw (auto-detect if omitted)").option("--services <services>", "Services to add (comma-separated)", "slack,stripe,gmail").option("--dry-run", "Preview changes without writing").action(async (opts) => {
11213
+ console.error("");
11214
+ console.error("\x1B[35m\x1B[1m \u25C8 Shadow Install\x1B[0m");
11215
+ console.error("");
11216
+ const services = opts.services.split(",").map((s) => s.trim()).filter(Boolean);
11217
+ const { client, configPath } = resolveClientConfig(opts.client);
11218
+ console.error(`\x1B[2m Client: ${client === "claude" ? "Claude Desktop" : "OpenClaw"}\x1B[0m`);
11219
+ console.error(`\x1B[2m Config: ${configPath}\x1B[0m`);
11220
+ console.error(`\x1B[2m Services: ${services.join(", ")}\x1B[0m`);
11221
+ console.error("");
11222
+ let config = {};
11223
+ if (existsSync(configPath)) {
11224
+ try {
11225
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
11226
+ } catch {
11227
+ console.error("\x1B[31m Error: Could not parse existing config file.\x1B[0m");
11228
+ process.exit(1);
11229
+ }
11230
+ }
11231
+ const entries = {};
11232
+ for (const svc of services) {
11233
+ entries[`shadow-${svc}`] = {
11234
+ command: "npx",
11235
+ args: ["-y", "mcp-shadow", "run", `--services=${svc}`, "--no-console"]
11236
+ };
11237
+ }
11238
+ if (client === "openclaw") {
11239
+ if (!config.provider)
11240
+ config.provider = {};
11241
+ const provider = config.provider;
11242
+ if (!provider.mcpServers)
11243
+ provider.mcpServers = {};
11244
+ Object.assign(provider.mcpServers, entries);
11245
+ } else {
11246
+ if (!config.mcpServers)
11247
+ config.mcpServers = {};
11248
+ Object.assign(config.mcpServers, entries);
11249
+ }
11250
+ if (opts.dryRun) {
11251
+ console.error("\x1B[2m Dry run \u2014 would write:\x1B[0m");
11252
+ console.error("");
11253
+ console.error(JSON.stringify(config, null, 2));
11254
+ console.error("");
11255
+ return;
11256
+ }
11257
+ mkdirSync(resolve(configPath, ".."), { recursive: true });
11258
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
11259
+ for (const svc of services) {
11260
+ console.error(` \x1B[32m\u2713\x1B[0m Added shadow-${svc}`);
11261
+ }
11262
+ console.error("");
11263
+ console.error(` Restart ${client === "claude" ? "Claude Desktop" : "OpenClaw"} to activate Shadow.`);
11264
+ console.error(" Run \x1B[2mshadow uninstall\x1B[0m to remove.");
11265
+ console.error("");
11266
+ });
11267
+ program2.command("uninstall").description("Remove Shadow MCP servers from your AI client config").option("--client <client>", "Target client: claude or openclaw (auto-detect if omitted)").action(async (opts) => {
11268
+ console.error("");
11269
+ console.error("\x1B[35m\x1B[1m \u25C8 Shadow Uninstall\x1B[0m");
11270
+ console.error("");
11271
+ const { client, configPath } = resolveClientConfig(opts.client);
11272
+ if (!existsSync(configPath)) {
11273
+ console.error("\x1B[2m No config file found. Nothing to remove.\x1B[0m");
11274
+ console.error("");
11275
+ return;
11276
+ }
11277
+ let config;
11278
+ try {
11279
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
11280
+ } catch {
11281
+ console.error("\x1B[31m Error: Could not parse config file.\x1B[0m");
11282
+ process.exit(1);
11283
+ }
11284
+ let mcpServers;
11285
+ if (client === "openclaw") {
11286
+ const provider = config.provider || {};
11287
+ mcpServers = provider.mcpServers || {};
11288
+ } else {
11289
+ mcpServers = config.mcpServers || {};
11290
+ }
11291
+ const removed = [];
11292
+ for (const key of Object.keys(mcpServers)) {
11293
+ if (key.startsWith("shadow-")) {
11294
+ delete mcpServers[key];
11295
+ removed.push(key);
11296
+ }
11297
+ }
11298
+ if (removed.length === 0) {
11299
+ console.error("\x1B[2m No Shadow entries found. Nothing to remove.\x1B[0m");
11300
+ console.error("");
11301
+ return;
11302
+ }
11303
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
11304
+ for (const key of removed) {
11305
+ console.error(` \x1B[32m\u2713\x1B[0m Removed ${key}`);
11306
+ }
11307
+ console.error("");
11308
+ console.error(` Restart ${client === "claude" ? "Claude Desktop" : "OpenClaw"} to apply.`);
11309
+ console.error("");
11310
+ });
11311
+ function checkPort(port) {
11312
+ return new Promise((resolve2) => {
11313
+ const server = createServer();
11314
+ server.once("error", () => resolve2(false));
11315
+ server.once("listening", () => {
11316
+ server.close();
11317
+ resolve2(true);
11318
+ });
11319
+ server.listen(port, "127.0.0.1");
11320
+ });
11321
+ }
11322
+ function resolveClientConfig(clientOpt) {
11323
+ const home = homedir();
11324
+ const configs = {
11325
+ "claude-darwin": resolve(home, "Library", "Application Support", "Claude", "claude_desktop_config.json"),
11326
+ "claude-linux": resolve(home, ".config", "Claude", "claude_desktop_config.json"),
11327
+ "claude-win32": resolve(process.env.APPDATA || resolve(home, "AppData", "Roaming"), "Claude", "claude_desktop_config.json"),
11328
+ "openclaw": resolve(home, ".openclaw", "openclaw.json")
11329
+ };
11330
+ if (clientOpt === "openclaw") {
11331
+ return { client: "openclaw", configPath: configs.openclaw };
11332
+ }
11333
+ if (clientOpt === "claude") {
11334
+ return { client: "claude", configPath: configs[`claude-${process.platform}`] || configs["claude-darwin"] };
11335
+ }
11336
+ if (existsSync(configs.openclaw)) {
11337
+ return { client: "openclaw", configPath: configs.openclaw };
11338
+ }
11339
+ return { client: "claude", configPath: configs[`claude-${process.platform}`] || configs["claude-darwin"] };
11340
+ }
11143
11341
  function getScenariosDir() {
11144
11342
  const bundled = resolve(__dirname, "..", "scenarios");
11145
11343
  if (existsSync(bundled))
@@ -11178,4 +11376,13 @@ function resolveProxyPath() {
11178
11376
  return monorepo;
11179
11377
  return null;
11180
11378
  }
11379
+ function resolveServerPath(service) {
11380
+ const bundled = resolve(__dirname, `server-${service}.js`);
11381
+ if (existsSync(bundled))
11382
+ return bundled;
11383
+ const monorepo = resolve(__dirname, "..", "..", `server-${service}`, "dist", "index.js");
11384
+ if (existsSync(monorepo))
11385
+ return monorepo;
11386
+ return null;
11387
+ }
11181
11388
  program2.parse();