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 +27 -16
- package/dist/cli.js +211 -4
- package/dist/console/assets/index-B9UDQRUl.js +43 -0
- package/dist/console/assets/index-Dzqqf6LR.css +1 -0
- package/dist/console/index.html +2 -2
- package/dist/demo-agent.cjs +15 -9
- package/dist/proxy.js +38 -9
- package/dist/server-slack.js +2 -2
- package/dist/server-stripe.js +6 -6
- package/package.json +1 -1
- package/scenarios/slack/angry-customer.yaml +1 -1
- package/scenarios/slack/data-leak.yaml +2 -2
- package/scenarios/slack/prompt-injection.yaml +1 -1
- package/dist/console/assets/index--m3D3yHT.js +0 -42
- package/dist/console/assets/index-CMWQ_I2S.css +0 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
199
|
-
shadow list
|
|
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 <
|
|
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
|
-
|
|
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
|
|
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();
|