@dotsetlabs/bellwether 2.1.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,6 +31,15 @@ bellwether check
31
31
 
32
32
  That's it. No API keys. No LLM costs. Runs in seconds.
33
33
 
34
+ ## Product Focus
35
+
36
+ Bellwether is intentionally opinionated:
37
+
38
+ - **Core workflow (default)**: `init` -> `check` -> `baseline`
39
+ - **Advanced workflow (opt-in)**: `explore`, `watch`, `discover`, `golden`, `contract`, `registry`
40
+
41
+ If you only need CI-safe drift detection, you can stay entirely in the core workflow.
42
+
34
43
  ## Two Modes
35
44
 
36
45
  | Mode | Purpose | Cost | When to Use |
@@ -85,45 +94,29 @@ jobs:
85
94
 
86
95
  Comparisons are **protocol-version-aware** — version-specific fields (annotations, titles, output schemas, etc.) are only compared when both baselines support the relevant MCP protocol version.
87
96
 
88
- ## Commands
89
-
90
- ### Essential Commands
91
-
92
- ```bash
93
- bellwether init <server-command> # Create config
94
- bellwether check # Detect drift (free, deterministic)
95
- bellwether baseline save # Save baseline to compare against
96
- bellwether baseline compare # Compare current vs saved baseline
97
- ```
97
+ ## Command Tiers
98
98
 
99
- ### Explore Command (Optional)
99
+ ### Core Commands (Recommended)
100
100
 
101
- ```bash
102
- bellwether init --preset local npx @mcp/server # Uses Ollama (free)
103
- bellwether explore # LLM-powered testing
104
- ```
105
-
106
- Requires LLM (Ollama for free local, or OpenAI/Anthropic). Generates `AGENTS.md` with behavioral documentation.
101
+ | Command | Purpose |
102
+ |:--------|:--------|
103
+ | `init` | Create `bellwether.yaml` |
104
+ | `check` | Deterministic schema drift detection |
105
+ | `baseline save` | Save snapshot for future comparisons |
106
+ | `baseline compare` | Compare latest check output to saved baseline |
107
107
 
108
- ### All Commands
108
+ ### Advanced Commands (Optional)
109
109
 
110
110
  | Command | Purpose |
111
111
  |:--------|:--------|
112
- | `init` | Create `bellwether.yaml` config |
113
- | `check` | Schema drift detection (free) |
114
- | `explore` | LLM behavioral testing |
115
- | `baseline save` | Save test results as baseline |
116
- | `baseline compare` | Compare against baseline |
117
- | `baseline show` | Display baseline contents |
118
- | `baseline accept` | Accept drift as intentional |
119
- | `baseline diff` | Compare two baselines |
120
- | `discover` | Show server capabilities |
112
+ | `explore` | LLM behavioral testing and `AGENTS.md` generation |
121
113
  | `watch` | Continuous checking on file changes |
114
+ | `discover` | Capability inspection without tests |
122
115
  | `registry` | Search MCP Registry |
123
116
  | `golden` | Golden output regression testing |
124
- | `contract` | Contract validation (generate/validate/show) |
117
+ | `contract` | Contract validation and generation |
125
118
  | `auth` | Manage LLM provider API keys |
126
- | `validate-config` | Validate bellwether.yaml without running tests |
119
+ | `validate-config` | Validate `bellwether.yaml` without running tests |
127
120
 
128
121
  ## CI/CD Exit Codes
129
122
 
@@ -139,9 +132,9 @@ Requires LLM (Ollama for free local, or OpenAI/Anthropic). Generates `AGENTS.md`
139
132
  ## GitHub Action
140
133
 
141
134
  ```yaml
142
- - uses: dotsetlabs/bellwether@v2.0.0
135
+ - uses: dotsetlabs/bellwether@v2.1.1
143
136
  with:
144
- version: '2.0.0'
137
+ version: '2.1.1'
145
138
  server-command: 'npx @mcp/your-server'
146
139
  baseline-path: './bellwether-baseline.json'
147
140
  fail-on-severity: 'warning'
@@ -170,8 +163,17 @@ bellwether init --preset local npx @mcp/server # Local Ollama (free)
170
163
  **[docs.bellwether.sh](https://docs.bellwether.sh)** — Full reference for configuration and commands.
171
164
 
172
165
  - [Quick Start](https://docs.bellwether.sh/quickstart)
166
+ - [Core vs Advanced](https://docs.bellwether.sh/concepts/core-vs-advanced)
173
167
  - [CLI Reference](https://docs.bellwether.sh/cli/init)
174
168
  - [CI/CD Integration](https://docs.bellwether.sh/guides/ci-cd)
169
+ - [Golden Paths](https://docs.bellwether.sh/guides/golden-paths)
170
+ - [Compatibility Policy](https://docs.bellwether.sh/concepts/compatibility-policy)
171
+
172
+ ## Project Governance
173
+
174
+ - [Roadmap](./ROADMAP.md)
175
+ - [Changelog](./CHANGELOG.md)
176
+ - [Security Policy](./SECURITY.md)
175
177
 
176
178
  ## Community
177
179
 
package/dist/cli/index.js CHANGED
@@ -86,42 +86,37 @@ Bellwether - MCP Server Validation & Documentation
86
86
  const examples = `
87
87
  Examples:
88
88
 
89
- Initialize configuration:
90
- $ bellwether init # Create bellwether.yaml
91
- $ bellwether init --preset ci # Optimized for CI/CD
92
- $ bellwether init --preset local # Local LLM with Ollama
89
+ Core workflow (recommended):
90
+ $ bellwether init npx @mcp/my-server # Create bellwether.yaml
91
+ $ bellwether check # Free, deterministic drift detection
92
+ $ bellwether baseline save # Save baseline snapshot
93
+ $ bellwether check --fail-on-drift # CI/CD gating
93
94
 
94
- Check for drift (free, fast, deterministic):
95
- $ bellwether check npx @mcp/my-server # Validate schemas
96
- $ bellwether baseline save # Save baseline
97
- $ bellwether baseline compare ./bellwether-baseline.json # Detect drift
98
-
99
- Explore behavior (LLM-powered):
100
- $ bellwether explore npx @mcp/my-server # Generate AGENTS.md documentation
101
-
102
- Discover server capabilities:
103
- $ bellwether discover npx @mcp/server-postgres
104
-
105
- Search MCP Registry:
106
- $ bellwether registry filesystem
95
+ Advanced workflow (opt-in):
96
+ $ bellwether explore # LLM behavioral exploration
97
+ $ bellwether discover # Quick capability inspection
98
+ $ bellwether watch # Continuous checking
107
99
 
108
100
  Documentation: https://docs.bellwether.sh
109
101
  `;
110
102
  program
111
103
  .name('bellwether')
112
104
  .description(`${banner}
113
- Check MCP servers for drift. Explore behavior. Generate documentation.
105
+ Deterministic MCP drift detection with an optional advanced analysis layer.
114
106
 
115
- Commands:
116
- check - Schema validation and drift detection (free, fast, deterministic)
117
- explore - LLM-powered behavioral exploration and documentation
118
- discover - Quick capability discovery (no tests)
119
- registry - Search the MCP Registry
120
- baseline - Manage baselines (save/compare/accept/diff/show)
121
- golden - Golden output regression testing
122
- contract - Contract validation (generate/validate/show)
123
- watch - Continuous checking on file changes
124
- auth - Manage LLM provider API keys
107
+ Core commands (default path):
108
+ init - Create bellwether.yaml configuration
109
+ check - Schema validation and drift detection (free, deterministic)
110
+ baseline - Save and compare baseline snapshots
111
+
112
+ Advanced commands (opt-in):
113
+ explore - LLM-powered behavioral exploration and documentation
114
+ discover - Quick capability discovery (no tests)
115
+ watch - Continuous checking on file changes
116
+ registry - Search the MCP Registry
117
+ golden - Golden output regression testing
118
+ contract - Contract validation (generate/validate/show)
119
+ auth - Manage LLM provider API keys
125
120
  validate-config - Validate bellwether.yaml without running tests
126
121
 
127
122
  For more information on a specific command, use:
@@ -155,19 +150,17 @@ For more information on a specific command, use:
155
150
  }
156
151
  })
157
152
  .addHelpText('after', examples);
158
- // Add command groups for better organization
159
- program.addHelpText('beforeAll', '\nCore Commands:');
160
- // Core commands - check and explore
153
+ program.addCommand(initCommand.description('Create a new bellwether.yaml configuration file'));
161
154
  program.addCommand(checkCommand.description('Check MCP server schema and detect drift (free, fast, deterministic)'));
155
+ program.addCommand(baselineCommand.description('Manage baselines for drift detection (save, compare, show, diff)'));
156
+ // Advanced commands
162
157
  program.addCommand(exploreCommand.description('Explore MCP server behavior with LLM-powered testing'));
163
158
  program.addCommand(watchCommand.description('Watch for MCP server changes and auto-check'));
164
159
  program.addCommand(discoverCommand.description('Discover MCP server capabilities (tools, prompts, resources)'));
165
- program.addCommand(initCommand.description('Create a new bellwether.yaml configuration file'));
166
- program.addCommand(authCommand.description('Manage LLM provider API keys (keychain storage)'));
167
- program.addCommand(baselineCommand.description('Manage baselines for drift detection (save, compare, show, diff)'));
168
160
  program.addCommand(goldenCommand.description('Manage golden outputs for tool validation (save, compare, list, delete)'));
169
161
  program.addCommand(registryCommand.description('Search the MCP Registry for servers'));
170
162
  program.addCommand(contractCommand.description('Validate MCP servers against contract definitions (validate, generate, show)'));
163
+ program.addCommand(authCommand.description('Manage LLM provider API keys (keychain storage)'));
171
164
  program.addCommand(validateConfigCommand.description('Validate bellwether.yaml configuration (no tests)'));
172
165
  // Custom help formatting
173
166
  program.configureHelp({
@@ -7,8 +7,8 @@ export declare const TIMEOUTS: {
7
7
  readonly WATCH_INTERVAL: 5000;
8
8
  /** Server startup delay (500ms) */
9
9
  readonly SERVER_STARTUP: 500;
10
- /** Minimum server startup wait (5 seconds) */
11
- readonly MIN_SERVER_STARTUP_WAIT: 5000;
10
+ /** Minimum server startup wait (500ms) */
11
+ readonly MIN_SERVER_STARTUP_WAIT: 500;
12
12
  /** Server ready poll interval (100ms) */
13
13
  readonly SERVER_READY_POLL: 100;
14
14
  /** Process shutdown SIGKILL timeout (5 seconds) */
@@ -7,8 +7,8 @@ export const TIMEOUTS = {
7
7
  WATCH_INTERVAL: 5000,
8
8
  /** Server startup delay (500ms) */
9
9
  SERVER_STARTUP: 500,
10
- /** Minimum server startup wait (5 seconds) */
11
- MIN_SERVER_STARTUP_WAIT: 5000,
10
+ /** Minimum server startup wait (500ms) */
11
+ MIN_SERVER_STARTUP_WAIT: 500,
12
12
  /** Server ready poll interval (100ms) */
13
13
  SERVER_READY_POLL: 100,
14
14
  /** Process shutdown SIGKILL timeout (5 seconds) */
@@ -1,11 +1,14 @@
1
1
  import pino from 'pino';
2
+ const IS_TEST_ENV = process.env.NODE_ENV === 'test' ||
3
+ process.env.VITEST === 'true' ||
4
+ process.env.VITEST_WORKER_ID !== undefined;
2
5
  /**
3
6
  * Default configuration.
4
7
  * Default level is 'warn' to keep CLI output clean.
5
8
  * Users can enable verbose output with --log-level info or --log-level debug.
6
9
  */
7
10
  const DEFAULT_CONFIG = {
8
- level: 'warn',
11
+ level: IS_TEST_ENV ? 'silent' : 'warn',
9
12
  pretty: false,
10
13
  timestamp: true,
11
14
  };
@@ -50,7 +53,7 @@ export function createLogger(config = {}) {
50
53
  */
51
54
  export function getLogger(name) {
52
55
  if (!globalLogger) {
53
- globalLogger = createLogger({ level: 'warn' });
56
+ globalLogger = createLogger({ level: DEFAULT_CONFIG.level });
54
57
  }
55
58
  if (name) {
56
59
  return globalLogger.child({ component: name });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Filter sensitive variables from process.env before spawning subprocesses.
3
+ * Explicitly provided additional environment variables are still allowed.
4
+ */
5
+ export declare function filterSpawnEnv(baseEnv: NodeJS.ProcessEnv, additionalEnv?: Record<string, string>): Record<string, string>;
6
+ //# sourceMappingURL=env-filter.d.ts.map
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Environment variables to filter out when spawning MCP server processes.
3
+ * These may contain sensitive credentials that should not be exposed.
4
+ */
5
+ const FILTERED_ENV_VARS = new Set([
6
+ // LLM API keys
7
+ 'OPENAI_API_KEY',
8
+ 'ANTHROPIC_API_KEY',
9
+ 'GOOGLE_API_KEY',
10
+ 'AZURE_OPENAI_API_KEY',
11
+ 'COHERE_API_KEY',
12
+ 'HUGGINGFACE_API_KEY',
13
+ 'REPLICATE_API_TOKEN',
14
+ // Provider credentials
15
+ 'AWS_SECRET_ACCESS_KEY',
16
+ 'AWS_SESSION_TOKEN',
17
+ 'AZURE_CLIENT_SECRET',
18
+ 'GOOGLE_APPLICATION_CREDENTIALS',
19
+ // SCM/CI tokens
20
+ 'GITHUB_TOKEN',
21
+ 'GH_TOKEN',
22
+ 'GITLAB_TOKEN',
23
+ 'BITBUCKET_TOKEN',
24
+ 'NPM_TOKEN',
25
+ 'PYPI_TOKEN',
26
+ // Database credentials
27
+ 'DATABASE_URL',
28
+ 'DATABASE_PASSWORD',
29
+ 'POSTGRES_PASSWORD',
30
+ 'MYSQL_PASSWORD',
31
+ 'REDIS_PASSWORD',
32
+ 'MONGODB_URI',
33
+ // Application secrets
34
+ 'COOKIE_SECRET',
35
+ 'SESSION_SECRET',
36
+ 'JWT_SECRET',
37
+ 'ENCRYPTION_KEY',
38
+ 'PRIVATE_KEY',
39
+ ]);
40
+ /**
41
+ * Patterns for environment variable names that should be filtered.
42
+ * Matches common naming conventions for secrets.
43
+ */
44
+ const FILTERED_ENV_PATTERNS = [
45
+ /_API_KEY$/i,
46
+ /_SECRET$/i,
47
+ /_TOKEN$/i,
48
+ /_PASSWORD$/i,
49
+ /_PRIVATE_KEY$/i,
50
+ /_CREDENTIALS$/i,
51
+ /^SECRET_/i,
52
+ /^PRIVATE_/i,
53
+ ];
54
+ function isSensitiveEnvVar(name) {
55
+ if (FILTERED_ENV_VARS.has(name)) {
56
+ return true;
57
+ }
58
+ return FILTERED_ENV_PATTERNS.some((pattern) => pattern.test(name));
59
+ }
60
+ /**
61
+ * Filter sensitive variables from process.env before spawning subprocesses.
62
+ * Explicitly provided additional environment variables are still allowed.
63
+ */
64
+ export function filterSpawnEnv(baseEnv, additionalEnv) {
65
+ const filtered = {};
66
+ for (const [key, value] of Object.entries(baseEnv)) {
67
+ if (value !== undefined && !isSensitiveEnvVar(key)) {
68
+ filtered[key] = value;
69
+ }
70
+ }
71
+ if (additionalEnv) {
72
+ Object.assign(filtered, additionalEnv);
73
+ }
74
+ return filtered;
75
+ }
76
+ //# sourceMappingURL=env-filter.js.map
@@ -86,15 +86,6 @@ export declare class MCPClient {
86
86
  */
87
87
  getTransportType(): TransportType;
88
88
  private log;
89
- /**
90
- * Check if an environment variable name looks like a secret.
91
- */
92
- private isSensitiveEnvVar;
93
- /**
94
- * Filter sensitive environment variables before passing to subprocess.
95
- * Uses both explicit list and pattern matching to catch common secret naming conventions.
96
- */
97
- private filterEnv;
98
89
  /**
99
90
  * Connect to an MCP server by spawning it as a subprocess.
100
91
  */
@@ -6,59 +6,7 @@ import { getLogger, startTiming } from '../logging/logger.js';
6
6
  import { TIMEOUTS, MCP, TRANSPORT_ERRORS } from '../constants.js';
7
7
  import { VERSION } from '../version.js';
8
8
  import { getFeatureFlags, isKnownProtocolVersion, } from '../protocol/index.js';
9
- /**
10
- * Environment variables to filter out when spawning MCP server processes.
11
- * These may contain sensitive credentials that should not be exposed.
12
- */
13
- const FILTERED_ENV_VARS = new Set([
14
- // LLM API keys
15
- 'OPENAI_API_KEY',
16
- 'ANTHROPIC_API_KEY',
17
- 'GOOGLE_API_KEY',
18
- 'AZURE_OPENAI_API_KEY',
19
- 'COHERE_API_KEY',
20
- 'HUGGINGFACE_API_KEY',
21
- 'REPLICATE_API_TOKEN',
22
- // Provider credentials
23
- 'AWS_SECRET_ACCESS_KEY',
24
- 'AWS_SESSION_TOKEN',
25
- 'AZURE_CLIENT_SECRET',
26
- 'GOOGLE_APPLICATION_CREDENTIALS',
27
- // SCM/CI tokens
28
- 'GITHUB_TOKEN',
29
- 'GH_TOKEN',
30
- 'GITLAB_TOKEN',
31
- 'BITBUCKET_TOKEN',
32
- 'NPM_TOKEN',
33
- 'PYPI_TOKEN',
34
- // Database credentials
35
- 'DATABASE_URL',
36
- 'DATABASE_PASSWORD',
37
- 'POSTGRES_PASSWORD',
38
- 'MYSQL_PASSWORD',
39
- 'REDIS_PASSWORD',
40
- 'MONGODB_URI',
41
- // Application secrets
42
- 'COOKIE_SECRET',
43
- 'SESSION_SECRET',
44
- 'JWT_SECRET',
45
- 'ENCRYPTION_KEY',
46
- 'PRIVATE_KEY',
47
- ]);
48
- /**
49
- * Patterns for environment variable names that should be filtered.
50
- * Matches common naming conventions for secrets.
51
- */
52
- const FILTERED_ENV_PATTERNS = [
53
- /_API_KEY$/i,
54
- /_SECRET$/i,
55
- /_TOKEN$/i,
56
- /_PASSWORD$/i,
57
- /_PRIVATE_KEY$/i,
58
- /_CREDENTIALS$/i,
59
- /^SECRET_/i,
60
- /^PRIVATE_/i,
61
- ];
9
+ import { filterSpawnEnv } from './env-filter.js';
62
10
  const DEFAULT_TIMEOUT = TIMEOUTS.DEFAULT;
63
11
  const DEFAULT_STARTUP_DELAY = TIMEOUTS.SERVER_STARTUP;
64
12
  /**
@@ -247,35 +195,6 @@ export class MCPClient {
247
195
  this.logger.debug({ args }, 'MCP Debug');
248
196
  }
249
197
  }
250
- /**
251
- * Check if an environment variable name looks like a secret.
252
- */
253
- isSensitiveEnvVar(name) {
254
- // Check explicit list
255
- if (FILTERED_ENV_VARS.has(name)) {
256
- return true;
257
- }
258
- // Check patterns
259
- return FILTERED_ENV_PATTERNS.some((pattern) => pattern.test(name));
260
- }
261
- /**
262
- * Filter sensitive environment variables before passing to subprocess.
263
- * Uses both explicit list and pattern matching to catch common secret naming conventions.
264
- */
265
- filterEnv(baseEnv, additionalEnv) {
266
- const filtered = {};
267
- // Copy process.env, filtering out sensitive variables
268
- for (const [key, value] of Object.entries(baseEnv)) {
269
- if (value !== undefined && !this.isSensitiveEnvVar(key)) {
270
- filtered[key] = value;
271
- }
272
- }
273
- // Add additional env vars (these are explicitly provided, so allow them)
274
- if (additionalEnv) {
275
- Object.assign(filtered, additionalEnv);
276
- }
277
- return filtered;
278
- }
279
198
  /**
280
199
  * Connect to an MCP server by spawning it as a subprocess.
281
200
  */
@@ -291,7 +210,7 @@ export class MCPClient {
291
210
  args,
292
211
  };
293
212
  // Filter out sensitive environment variables before spawning subprocess
294
- const filteredEnv = this.filterEnv(process.env, env);
213
+ const filteredEnv = filterSpawnEnv(process.env, env);
295
214
  this.process = spawn(command, args, {
296
215
  stdio: ['pipe', 'pipe', 'pipe'],
297
216
  env: filteredEnv,
@@ -418,7 +337,7 @@ export class MCPClient {
418
337
  */
419
338
  async waitForStartup() {
420
339
  // Enforce minimum startup delay to allow server to fully start
421
- // npx-based servers often need significant time to download and start
340
+ // while still honoring explicit higher startupDelay values from config/tests.
422
341
  const delay = Math.max(this.startupDelay, TIMEOUTS.MIN_SERVER_STARTUP_WAIT);
423
342
  this.logger.debug({ delay }, 'Waiting for server startup');
424
343
  await new Promise((resolve) => setTimeout(resolve, delay));
package/dist/version.js CHANGED
@@ -29,8 +29,8 @@ function getPackageVersion() {
29
29
  return packageJson.version;
30
30
  }
31
31
  catch {
32
- // Fallback version - should match package.json
33
- return '2.1.0';
32
+ // Final fallback to avoid hardcoded release drift.
33
+ return process.env.BELLWETHER_VERSION || '0.0.0';
34
34
  }
35
35
  }
36
36
  /**
package/man/bellwether.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH "BELLWETHER" "1" "2026\-02\-11" "Bellwether 2.1.0" "User Commands"
1
+ .TH "BELLWETHER" "1" "2026\-02\-14" "Bellwether 2.1.1" "User Commands"
2
2
  .SH NAME
3
3
  .PP
4
4
  bellwether \[em] MCP server testing and validation tool
@@ -2,8 +2,8 @@
2
2
  title: BELLWETHER
3
3
  section: 1
4
4
  header: User Commands
5
- footer: Bellwether 2.1.0
6
- date: 2026-02-11
5
+ footer: Bellwether 2.1.1
6
+ date: 2026-02-14
7
7
  ---
8
8
 
9
9
  # NAME
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotsetlabs/bellwether",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "The open-source MCP testing tool. Structural drift detection and behavioral documentation for Model Context Protocol servers.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc",
26
- "postbuild": "chmod +x dist/cli/index.js",
26
+ "postbuild": "node ./scripts/postbuild.mjs",
27
27
  "dev": "tsc --watch",
28
28
  "test": "vitest run",
29
29
  "test:watch": "vitest",
@@ -32,6 +32,7 @@
32
32
  "lint:fix": "eslint src --ext .ts --fix",
33
33
  "format": "prettier --write \"src/**/*.ts\"",
34
34
  "format:check": "prettier --check \"src/**/*.ts\"",
35
+ "check:consistency": "node ./scripts/validate-consistency.mjs",
35
36
  "clean": "rm -rf dist",
36
37
  "docs:generate": "npm --prefix website run build",
37
38
  "docs:dev": "npm --prefix website run start",