@dotsetlabs/bellwether 2.1.1 → 2.1.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +19 -4
  3. package/dist/cli/commands/check.js +49 -6
  4. package/dist/cli/commands/dashboard.d.ts +3 -0
  5. package/dist/cli/commands/dashboard.js +69 -0
  6. package/dist/cli/commands/discover.js +24 -2
  7. package/dist/cli/commands/explore.js +49 -6
  8. package/dist/cli/commands/watch.js +12 -1
  9. package/dist/cli/utils/headers.d.ts +12 -0
  10. package/dist/cli/utils/headers.js +63 -0
  11. package/dist/config/defaults.d.ts +2 -0
  12. package/dist/config/defaults.js +2 -0
  13. package/dist/config/template.js +12 -0
  14. package/dist/config/validator.d.ts +38 -18
  15. package/dist/config/validator.js +10 -0
  16. package/dist/constants/core.d.ts +2 -0
  17. package/dist/constants/core.js +11 -0
  18. package/dist/dashboard/index.d.ts +3 -0
  19. package/dist/dashboard/index.js +6 -0
  20. package/dist/dashboard/runtime/artifact-index.d.ts +45 -0
  21. package/dist/dashboard/runtime/artifact-index.js +238 -0
  22. package/dist/dashboard/runtime/command-profiles.d.ts +764 -0
  23. package/dist/dashboard/runtime/command-profiles.js +691 -0
  24. package/dist/dashboard/runtime/config-service.d.ts +21 -0
  25. package/dist/dashboard/runtime/config-service.js +73 -0
  26. package/dist/dashboard/runtime/job-runner.d.ts +26 -0
  27. package/dist/dashboard/runtime/job-runner.js +292 -0
  28. package/dist/dashboard/security/input-validation.d.ts +3 -0
  29. package/dist/dashboard/security/input-validation.js +27 -0
  30. package/dist/dashboard/security/localhost-guard.d.ts +5 -0
  31. package/dist/dashboard/security/localhost-guard.js +52 -0
  32. package/dist/dashboard/server.d.ts +14 -0
  33. package/dist/dashboard/server.js +293 -0
  34. package/dist/dashboard/types.d.ts +55 -0
  35. package/dist/dashboard/types.js +2 -0
  36. package/dist/dashboard/ui.d.ts +2 -0
  37. package/dist/dashboard/ui.js +2264 -0
  38. package/dist/discovery/discovery.js +20 -1
  39. package/dist/discovery/types.d.ts +1 -1
  40. package/dist/docs/contract.js +7 -1
  41. package/dist/errors/retry.js +15 -1
  42. package/dist/errors/types.d.ts +10 -0
  43. package/dist/errors/types.js +28 -0
  44. package/dist/transport/http-transport.js +10 -0
  45. package/dist/transport/mcp-client.d.ts +16 -0
  46. package/dist/transport/mcp-client.js +116 -4
  47. package/dist/transport/sse-transport.js +19 -0
  48. package/package.json +3 -14
  49. package/man/bellwether.1 +0 -204
  50. package/man/bellwether.1.md +0 -148
@@ -8,6 +8,7 @@ const logger = getLogger('discovery');
8
8
  export async function discover(client, command, args) {
9
9
  // Initialize connection
10
10
  const initResult = await client.initialize();
11
+ const capabilityWarnings = [];
11
12
  // Discover tools
12
13
  let tools = [];
13
14
  if (initResult.capabilities.tools) {
@@ -16,6 +17,7 @@ export async function discover(client, command, args) {
16
17
  }
17
18
  catch (error) {
18
19
  logger.error({ error }, 'Failed to list tools');
20
+ throw new Error(`Failed to list tools despite advertised tools capability: ${error instanceof Error ? error.message : String(error)}`);
19
21
  }
20
22
  }
21
23
  // Discover prompts
@@ -26,6 +28,11 @@ export async function discover(client, command, args) {
26
28
  }
27
29
  catch (error) {
28
30
  logger.error({ error }, 'Failed to list prompts');
31
+ capabilityWarnings.push({
32
+ level: 'warning',
33
+ message: `Failed to list prompts: ${error instanceof Error ? error.message : String(error)}`,
34
+ recommendation: 'Check server prompt implementation and transport health.',
35
+ });
29
36
  }
30
37
  }
31
38
  // Discover resources
@@ -37,18 +44,30 @@ export async function discover(client, command, args) {
37
44
  }
38
45
  catch (error) {
39
46
  logger.error({ error }, 'Failed to list resources');
47
+ capabilityWarnings.push({
48
+ level: 'warning',
49
+ message: `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`,
50
+ recommendation: 'Check server resource implementation and transport health.',
51
+ });
40
52
  }
41
53
  try {
42
54
  resourceTemplates = await client.listResourceTemplates();
43
55
  }
44
56
  catch (error) {
45
57
  logger.debug({ error }, 'Failed to list resource templates (server may not support them)');
58
+ capabilityWarnings.push({
59
+ level: 'info',
60
+ message: `Failed to list resource templates: ${error instanceof Error ? error.message : String(error)}`,
61
+ });
46
62
  }
47
63
  }
48
64
  // Collect transport errors from the client
49
65
  const transportErrors = client.getTransportErrors();
50
66
  // Generate warnings based on discovery results
51
- const warnings = generateDiscoveryWarnings(initResult.capabilities, tools, prompts, resources, transportErrors, initResult.protocolVersion);
67
+ const warnings = [
68
+ ...capabilityWarnings,
69
+ ...generateDiscoveryWarnings(initResult.capabilities, tools, prompts, resources, transportErrors, initResult.protocolVersion),
70
+ ];
52
71
  return {
53
72
  serverInfo: initResult.serverInfo,
54
73
  protocolVersion: initResult.protocolVersion,
@@ -60,7 +60,7 @@ export interface PropertySchema {
60
60
  * Classification of transport-level errors.
61
61
  * Used to differentiate between server bugs, protocol issues, and environment problems.
62
62
  */
63
- export type TransportErrorCategory = 'invalid_json' | 'buffer_overflow' | 'connection_refused' | 'connection_lost' | 'protocol_violation' | 'timeout' | 'shutdown_error' | 'unknown';
63
+ export type TransportErrorCategory = 'invalid_json' | 'buffer_overflow' | 'connection_refused' | 'auth_failed' | 'connection_lost' | 'protocol_violation' | 'timeout' | 'shutdown_error' | 'unknown';
64
64
  /**
65
65
  * Record of a transport-level error that occurred during MCP communication.
66
66
  */
@@ -624,9 +624,13 @@ function generateTransportIssuesSection(transportErrors, warnings) {
624
624
  // Recommendations
625
625
  const hasInvalidJson = transportErrors.some((e) => e.category === 'invalid_json');
626
626
  const hasProtocolError = transportErrors.some((e) => e.category === 'protocol_violation');
627
- if (hasInvalidJson || hasProtocolError) {
627
+ const hasAuthError = transportErrors.some((e) => e.category === 'auth_failed');
628
+ if (hasInvalidJson || hasProtocolError || hasAuthError) {
628
629
  lines.push('### Recommendations');
629
630
  lines.push('');
631
+ if (hasAuthError) {
632
+ lines.push('- **Auth Failed**: Configure remote authentication headers (for example `Authorization: Bearer <token>`) and verify credentials are valid.');
633
+ }
630
634
  if (hasInvalidJson) {
631
635
  lines.push('- **Invalid JSON**: The server may be writing debug output to stdout. Ensure all non-JSON-RPC output goes to stderr.');
632
636
  }
@@ -649,6 +653,8 @@ function formatTransportErrorCategory(category) {
649
653
  return 'Buffer Overflow';
650
654
  case 'connection_refused':
651
655
  return 'Connection Refused';
656
+ case 'auth_failed':
657
+ return 'Auth Failed';
652
658
  case 'connection_lost':
653
659
  return 'Connection Lost';
654
660
  case 'protocol_violation':
@@ -2,7 +2,7 @@
2
2
  * Retry logic with exponential backoff.
3
3
  */
4
4
  import { getLogger } from '../logging/logger.js';
5
- import { BellwetherError, LLMRateLimitError, isRetryable, wrapError, createTimingContext, } from './types.js';
5
+ import { BellwetherError, LLMRateLimitError, ServerAuthError, isRetryable, wrapError, createTimingContext, } from './types.js';
6
6
  import { RETRY_STRATEGIES, CIRCUIT_BREAKER, MATH_FACTORS } from '../constants.js';
7
7
  const DEFAULT_OPTIONS = {
8
8
  maxAttempts: RETRY_STRATEGIES.DEFAULT.maxAttempts,
@@ -191,7 +191,21 @@ export const TRANSPORT_RETRY_OPTIONS = {
191
191
  backoffMultiplier: RETRY_STRATEGIES.TRANSPORT.backoffMultiplier,
192
192
  jitter: RETRY_STRATEGIES.TRANSPORT.jitter,
193
193
  shouldRetry: (error) => {
194
+ if (error instanceof ServerAuthError) {
195
+ return false;
196
+ }
194
197
  const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
198
+ // Authentication/authorization failures are terminal until credentials change
199
+ if (message.includes('401') ||
200
+ message.includes('403') ||
201
+ message.includes('407') ||
202
+ message.includes('unauthorized') ||
203
+ message.includes('forbidden') ||
204
+ message.includes('authentication') ||
205
+ message.includes('authorization') ||
206
+ message.includes('access denied')) {
207
+ return false;
208
+ }
195
209
  // Timeouts might be transient
196
210
  if (message.includes('timeout')) {
197
211
  return true;
@@ -120,6 +120,16 @@ export declare class ServerExitError extends TransportError {
120
120
  export declare class ProtocolError extends TransportError {
121
121
  constructor(message: string, context?: ErrorContext, cause?: Error);
122
122
  }
123
+ /**
124
+ * Remote server authentication/authorization failed.
125
+ */
126
+ export declare class ServerAuthError extends TransportError {
127
+ /** HTTP status code if available */
128
+ readonly statusCode?: number;
129
+ /** Suggested remediation hint */
130
+ readonly hint?: string;
131
+ constructor(message: string, statusCode?: number, hint?: string, context?: ErrorContext, cause?: Error);
132
+ }
123
133
  /**
124
134
  * Buffer overflow during message processing.
125
135
  */
@@ -146,6 +146,34 @@ export class ProtocolError extends TransportError {
146
146
  this.name = 'ProtocolError';
147
147
  }
148
148
  }
149
+ /**
150
+ * Remote server authentication/authorization failed.
151
+ */
152
+ export class ServerAuthError extends TransportError {
153
+ /** HTTP status code if available */
154
+ statusCode;
155
+ /** Suggested remediation hint */
156
+ hint;
157
+ constructor(message, statusCode, hint, context, cause) {
158
+ super(message, {
159
+ code: 'TRANSPORT_AUTH_FAILED',
160
+ severity: 'high',
161
+ retryable: 'terminal',
162
+ context: {
163
+ ...context,
164
+ metadata: {
165
+ ...context?.metadata,
166
+ statusCode,
167
+ hint,
168
+ },
169
+ },
170
+ cause,
171
+ });
172
+ this.name = 'ServerAuthError';
173
+ this.statusCode = statusCode;
174
+ this.hint = hint;
175
+ }
176
+ }
149
177
  /**
150
178
  * Buffer overflow during message processing.
151
179
  */
@@ -1,5 +1,6 @@
1
1
  import { BaseTransport } from './base-transport.js';
2
2
  import { TIMEOUTS, DISPLAY_LIMITS, MCP } from '../constants.js';
3
+ import { ServerAuthError } from '../errors/types.js';
3
4
  /**
4
5
  * HTTPTransport connects to MCP servers over HTTP using POST requests.
5
6
  *
@@ -111,6 +112,15 @@ export class HTTPTransport extends BaseTransport {
111
112
  });
112
113
  clearTimeout(timeoutId);
113
114
  if (!response.ok) {
115
+ if (response.status === 401) {
116
+ throw new ServerAuthError('Remote MCP server authentication failed (401 Unauthorized)', 401, 'Add server.headers.Authorization (for example: Bearer token) in bellwether.yaml or pass --header.');
117
+ }
118
+ if (response.status === 403) {
119
+ throw new ServerAuthError('Remote MCP server authorization failed (403 Forbidden)', 403, 'Credentials are recognized but lack required permissions. Verify token scopes/roles.');
120
+ }
121
+ if (response.status === 407) {
122
+ throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
123
+ }
114
124
  // MCP 2025-11-25: 404 means session expired, clear session ID
115
125
  if (response.status === 404 && this.sessionId) {
116
126
  this.log('Session expired (404), clearing session ID');
@@ -19,6 +19,8 @@ export interface MCPClientOptions {
19
19
  sseConfig?: Omit<SSETransportConfig, 'debug'>;
20
20
  /** Configuration for HTTP transport */
21
21
  httpConfig?: Omit<HTTPTransportConfig, 'debug'>;
22
+ /** Optional preflight check for remote transports (default: true) */
23
+ remotePreflight?: boolean;
22
24
  }
23
25
  /**
24
26
  * MCPClient connects to an MCP server via various transports and provides
@@ -55,7 +57,21 @@ export declare class MCPClient {
55
57
  private connectionState;
56
58
  /** Protocol version negotiated with the server during initialization */
57
59
  private negotiatedProtocolVersion;
60
+ /** Whether to run an explicit preflight before remote connection */
61
+ private remotePreflight;
58
62
  constructor(options?: MCPClientOptions);
63
+ /**
64
+ * Merge two header maps, giving precedence to override values.
65
+ */
66
+ private mergeHeaders;
67
+ /**
68
+ * Optional remote connectivity/auth preflight (enabled by default).
69
+ * Uses GET (not HEAD/OPTIONS) for broader compatibility.
70
+ *
71
+ * Any successful HTTP response (including 404/405/etc.) confirms network reachability.
72
+ * Only auth failures and hard network/timeouts are treated as terminal preflight failures.
73
+ */
74
+ private preflightRemote;
59
75
  /**
60
76
  * Classify a transport error based on its message.
61
77
  */
@@ -7,6 +7,7 @@ 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
9
  import { filterSpawnEnv } from './env-filter.js';
10
+ import { ConnectionError, ServerAuthError } from '../errors/types.js';
10
11
  const DEFAULT_TIMEOUT = TIMEOUTS.DEFAULT;
11
12
  const DEFAULT_STARTUP_DELAY = TIMEOUTS.SERVER_STARTUP;
12
13
  /**
@@ -44,6 +45,8 @@ export class MCPClient {
44
45
  connectionState = { attempted: false };
45
46
  /** Protocol version negotiated with the server during initialization */
46
47
  negotiatedProtocolVersion = null;
48
+ /** Whether to run an explicit preflight before remote connection */
49
+ remotePreflight;
47
50
  constructor(options) {
48
51
  this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
49
52
  this.startupDelay = options?.startupDelay ?? DEFAULT_STARTUP_DELAY;
@@ -51,6 +54,96 @@ export class MCPClient {
51
54
  this.transportType = options?.transport ?? 'stdio';
52
55
  this.sseConfig = options?.sseConfig;
53
56
  this.httpConfig = options?.httpConfig;
57
+ this.remotePreflight = options?.remotePreflight ?? true;
58
+ }
59
+ /**
60
+ * Merge two header maps, giving precedence to override values.
61
+ */
62
+ mergeHeaders(base, override) {
63
+ if (!base && !override)
64
+ return undefined;
65
+ const merged = {};
66
+ const apply = (headers) => {
67
+ if (!headers)
68
+ return;
69
+ for (const [name, value] of Object.entries(headers)) {
70
+ const normalized = name.toLowerCase();
71
+ for (const existing of Object.keys(merged)) {
72
+ if (existing.toLowerCase() === normalized) {
73
+ delete merged[existing];
74
+ break;
75
+ }
76
+ }
77
+ merged[name] = value;
78
+ }
79
+ };
80
+ apply(base);
81
+ apply(override);
82
+ return Object.keys(merged).length > 0 ? merged : undefined;
83
+ }
84
+ /**
85
+ * Optional remote connectivity/auth preflight (enabled by default).
86
+ * Uses GET (not HEAD/OPTIONS) for broader compatibility.
87
+ *
88
+ * Any successful HTTP response (including 404/405/etc.) confirms network reachability.
89
+ * Only auth failures and hard network/timeouts are treated as terminal preflight failures.
90
+ */
91
+ async preflightRemote(url, transport, headers) {
92
+ if (!this.remotePreflight) {
93
+ return;
94
+ }
95
+ const endpoint = transport === 'sse' ? `${url.replace(/\/$/, '')}/sse` : url;
96
+ const controller = new AbortController();
97
+ const timeoutId = setTimeout(() => controller.abort(), Math.min(this.timeout, 5000));
98
+ try {
99
+ const response = await fetch(endpoint, {
100
+ method: 'GET',
101
+ headers: {
102
+ Accept: transport === 'sse' ? 'text/event-stream' : 'application/json, text/event-stream',
103
+ ...(headers ?? {}),
104
+ },
105
+ signal: controller.signal,
106
+ });
107
+ const closePreflightBody = async () => {
108
+ try {
109
+ await response.body?.cancel();
110
+ }
111
+ catch {
112
+ // Ignore body cancellation failures; preflight status handling is primary.
113
+ }
114
+ };
115
+ if (response.status === 401) {
116
+ await closePreflightBody();
117
+ throw new ServerAuthError(`Remote MCP preflight failed: unauthorized (${response.status})`, response.status, 'Add authentication headers (for example Authorization) and retry.');
118
+ }
119
+ if (response.status === 403) {
120
+ await closePreflightBody();
121
+ throw new ServerAuthError(`Remote MCP preflight failed: forbidden (${response.status})`, response.status, 'Credentials are valid but lack required permissions/scopes.');
122
+ }
123
+ if (response.status === 407) {
124
+ await closePreflightBody();
125
+ throw new ServerAuthError(`Remote MCP preflight failed: proxy authentication required (${response.status})`, response.status, 'Configure proxy credentials and retry.');
126
+ }
127
+ if (!response.ok) {
128
+ // Non-auth HTTP statuses are compatibility-safe to continue:
129
+ // some MCP servers reject generic GET preflight paths but still
130
+ // support their actual protocol endpoints for normal operation.
131
+ this.logger.debug({ endpoint, status: response.status }, 'Remote preflight received non-auth HTTP status; continuing to transport connect');
132
+ }
133
+ await closePreflightBody();
134
+ }
135
+ catch (error) {
136
+ if (error instanceof ServerAuthError || error instanceof ConnectionError) {
137
+ throw error;
138
+ }
139
+ if (error instanceof Error && error.name === 'AbortError') {
140
+ throw new ConnectionError(`Remote MCP preflight timed out for ${endpoint}`);
141
+ }
142
+ throw new ConnectionError(`Remote MCP preflight failed for ${endpoint}: ${error instanceof Error ? error.message : String(error)}`);
143
+ }
144
+ finally {
145
+ clearTimeout(timeoutId);
146
+ }
54
147
  }
55
148
  /**
56
149
  * Classify a transport error based on its message.
@@ -75,6 +168,12 @@ export class MCPClient {
75
168
  return { category: 'connection_refused', likelyServerBug: false };
76
169
  }
77
170
  }
171
+ // Check for authentication failures (environment/config issue)
172
+ for (const pattern of TRANSPORT_ERRORS.AUTH_FAILURE_PATTERNS) {
173
+ if (pattern.test(msg)) {
174
+ return { category: 'auth_failed', likelyServerBug: false };
175
+ }
176
+ }
78
177
  // Check for connection lost
79
178
  for (const pattern of TRANSPORT_ERRORS.CONNECTION_LOST_PATTERNS) {
80
179
  if (pattern.test(msg)) {
@@ -138,6 +237,8 @@ export class MCPClient {
138
237
  return 'Server response exceeded buffer limits';
139
238
  case 'connection_refused':
140
239
  return 'Failed to connect to server process';
240
+ case 'auth_failed':
241
+ return 'Authentication failed when connecting to remote MCP server';
141
242
  case 'connection_lost':
142
243
  return 'Connection to server was lost unexpectedly';
143
244
  case 'protocol_violation':
@@ -228,6 +329,7 @@ export class MCPClient {
228
329
  this.transport.on('error', (error) => {
229
330
  this.logger.error({ error: error.message }, 'Transport error');
230
331
  this.recordTransportError(error, 'stdio_transport');
332
+ this.clearPendingRequests(error.message);
231
333
  });
232
334
  this.transport.on('close', () => {
233
335
  this.logger.debug('Transport closed');
@@ -279,27 +381,36 @@ export class MCPClient {
279
381
  // Reset flags for new connection
280
382
  this.cleaningUp = false;
281
383
  this.disconnecting = false;
384
+ // Track connection state for diagnostics
385
+ this.connectionState = {
386
+ attempted: true,
387
+ url,
388
+ };
282
389
  this.logger.info({ url, transport }, 'Connecting to remote MCP server');
390
+ const mergedHeaders = transport === 'sse'
391
+ ? this.mergeHeaders(this.sseConfig?.headers, options?.headers)
392
+ : this.mergeHeaders(this.httpConfig?.headers, options?.headers);
393
+ await this.preflightRemote(url, transport, mergedHeaders);
283
394
  if (transport === 'sse') {
284
395
  const sseTransport = new SSETransport({
396
+ ...this.sseConfig,
285
397
  baseUrl: url,
286
398
  sessionId: options?.sessionId ?? this.sseConfig?.sessionId,
287
- headers: options?.headers ?? this.sseConfig?.headers,
399
+ headers: mergedHeaders,
288
400
  timeout: this.timeout,
289
401
  debug: this.debug,
290
- ...this.sseConfig,
291
402
  });
292
403
  await sseTransport.connect();
293
404
  this.transport = sseTransport;
294
405
  }
295
406
  else if (transport === 'streamable-http') {
296
407
  const httpTransport = new HTTPTransport({
408
+ ...this.httpConfig,
297
409
  baseUrl: url,
298
410
  sessionId: options?.sessionId ?? this.httpConfig?.sessionId,
299
- headers: options?.headers ?? this.httpConfig?.headers,
411
+ headers: mergedHeaders,
300
412
  timeout: this.timeout,
301
413
  debug: this.debug,
302
- ...this.httpConfig,
303
414
  });
304
415
  await httpTransport.connect();
305
416
  this.transport = httpTransport;
@@ -324,6 +435,7 @@ export class MCPClient {
324
435
  this.transport.on('error', (error) => {
325
436
  this.logger.error({ error: error.message }, 'Transport error');
326
437
  this.recordTransportError(error, 'remote_transport');
438
+ this.clearPendingRequests(error.message);
327
439
  });
328
440
  this.transport.on('close', () => {
329
441
  this.logger.debug('Transport closed');
@@ -1,6 +1,7 @@
1
1
  import { BaseTransport } from './base-transport.js';
2
2
  import { TIME_CONSTANTS, TIMEOUTS } from '../constants.js';
3
3
  import { isLocalhost } from '../utils/index.js';
4
+ import { ServerAuthError } from '../errors/types.js';
4
5
  /**
5
6
  * Validate that a URL uses HTTPS in production contexts.
6
7
  * Allows HTTP only for localhost/127.0.0.1 for local development.
@@ -161,6 +162,15 @@ export class SSETransport extends BaseTransport {
161
162
  signal: this.streamAbortController.signal,
162
163
  });
163
164
  if (!response.ok) {
165
+ if (response.status === 401) {
166
+ throw new ServerAuthError('Remote MCP SSE authentication failed (401 Unauthorized)', 401, 'Add server.headers.Authorization (for example: Bearer token) in bellwether.yaml or pass --header.');
167
+ }
168
+ if (response.status === 403) {
169
+ throw new ServerAuthError('Remote MCP SSE authorization failed (403 Forbidden)', 403, 'Credentials are recognized but lack required permissions. Verify token scopes/roles.');
170
+ }
171
+ if (response.status === 407) {
172
+ throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
173
+ }
164
174
  throw new Error(`Failed to connect to SSE endpoint: HTTP ${response.status}`);
165
175
  }
166
176
  if (!response.body) {
@@ -334,6 +344,15 @@ export class SSETransport extends BaseTransport {
334
344
  .then(async (response) => {
335
345
  clearTimeout(timeoutId);
336
346
  if (!response.ok) {
347
+ if (response.status === 401) {
348
+ throw new ServerAuthError('Remote MCP message authentication failed (401 Unauthorized)', 401, 'Add server.headers.Authorization (for example: Bearer token) in bellwether.yaml or pass --header.');
349
+ }
350
+ if (response.status === 403) {
351
+ throw new ServerAuthError('Remote MCP message authorization failed (403 Forbidden)', 403, 'Credentials are recognized but lack required permissions. Verify token scopes/roles.');
352
+ }
353
+ if (response.status === 407) {
354
+ throw new ServerAuthError('Proxy authentication required (407)', 407, 'Configure proxy credentials and retry.');
355
+ }
337
356
  const errorText = await response.text().catch(() => 'Unknown error');
338
357
  throw new Error(`HTTP ${response.status}: ${errorText}`);
339
358
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotsetlabs/bellwether",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
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",
@@ -36,9 +36,7 @@
36
36
  "clean": "rm -rf dist",
37
37
  "docs:generate": "npm --prefix website run build",
38
38
  "docs:dev": "npm --prefix website run start",
39
- "man:generate": "./scripts/generate-manpage.sh",
40
- "prepare": "husky install || true",
41
- "prepublishOnly": "npm run build && npm run man:generate"
39
+ "prepublishOnly": "npm run build"
42
40
  },
43
41
  "keywords": [
44
42
  "mcp",
@@ -75,7 +73,7 @@
75
73
  "node": ">=20.0.0"
76
74
  },
77
75
  "dependencies": {
78
- "@anthropic-ai/sdk": "^0.72.1",
76
+ "@anthropic-ai/sdk": "^0.74.0",
79
77
  "ajv": "^8.17.1",
80
78
  "chalk": "^5.4.1",
81
79
  "cli-progress": "^3.12.0",
@@ -98,25 +96,16 @@
98
96
  "@typescript-eslint/parser": "^6.21.0",
99
97
  "@vitest/coverage-v8": "^4.0.18",
100
98
  "eslint": "^8.57.1",
101
- "husky": "^9.1.0",
102
- "lint-staged": "^16.2.7",
103
99
  "prettier": "^3.3.0",
104
100
  "tsx": "^4.21.0",
105
101
  "typescript": "^5.3.0",
106
102
  "vitest": "^4.0.17"
107
103
  },
108
- "lint-staged": {
109
- "*.ts": [
110
- "eslint --fix",
111
- "prettier --write"
112
- ]
113
- },
114
104
  "files": [
115
105
  "dist",
116
106
  "!dist/**/*.map",
117
107
  "schemas",
118
108
  "scripts/completions",
119
- "man",
120
109
  "README.md",
121
110
  "LICENSE",
122
111
  "CHANGELOG.md",