@mcpspec/core 1.0.2 → 1.0.3

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/dist/index.d.ts CHANGED
@@ -352,6 +352,7 @@ declare class ResultDiffer {
352
352
  diff(baseline: TestRunResult, current: TestRunResult, baselineName?: string): RunDiff;
353
353
  }
354
354
 
355
+ declare const DANGEROUS_TOOL_PATTERNS: RegExp;
355
356
  declare class ScanConfig {
356
357
  readonly mode: SecurityScanMode;
357
358
  readonly rules: string[];
@@ -359,9 +360,12 @@ declare class ScanConfig {
359
360
  readonly acknowledgeRisk: boolean;
360
361
  readonly timeout: number;
361
362
  readonly maxProbesPerTool: number;
363
+ readonly excludeTools: string[];
364
+ readonly dryRun: boolean;
362
365
  constructor(config?: Partial<SecurityScanConfig>);
363
366
  requiresConfirmation(): boolean;
364
367
  meetsThreshold(severity: SeverityLevel): boolean;
368
+ isToolExcluded(toolName: string): boolean;
365
369
  private getRulesForMode;
366
370
  }
367
371
 
@@ -377,10 +381,27 @@ interface ScanProgress {
377
381
  onRuleComplete?: (ruleId: string, findingCount: number) => void;
378
382
  onFinding?: (finding: SecurityFinding) => void;
379
383
  }
384
+ interface DryRunResult {
385
+ tools: Array<{
386
+ name: string;
387
+ included: boolean;
388
+ reason?: string;
389
+ }>;
390
+ rules: string[];
391
+ mode: string;
392
+ }
380
393
  declare class SecurityScanner {
381
394
  private readonly rules;
382
395
  constructor();
383
396
  registerRule(rule: SecurityRule): void;
397
+ /**
398
+ * Preview which tools will be scanned without actually running payloads.
399
+ */
400
+ dryRun(client: MCPClientInterface, config: ScanConfig): Promise<DryRunResult>;
401
+ /**
402
+ * Filter tools based on config exclusions.
403
+ */
404
+ filterTools(tools: ToolInfo[], config: ScanConfig): ToolInfo[];
384
405
  scan(client: MCPClientInterface, config: ScanConfig, progress?: ScanProgress): Promise<SecurityScanResult>;
385
406
  private buildSummary;
386
407
  private registerBuiltinRules;
@@ -514,7 +535,7 @@ declare class MCPScoreCalculator {
514
535
  private scoreDocumentation;
515
536
  private scoreSchemaQuality;
516
537
  private scoreErrorHandling;
517
- private scorePerformance;
538
+ private scoreResponsiveness;
518
539
  private scoreSecurity;
519
540
  }
520
541
 
@@ -523,4 +544,4 @@ declare class BadgeGenerator {
523
544
  getColor(score: number): string;
524
545
  }
525
546
 
526
- export { AuthBypassRule, BadgeGenerator, BaselineStore, type BenchmarkProgress, BenchmarkRunner, ConnectionManager, ConsoleReporter, DocGenerator, type DocGeneratorOptions, ERROR_CODE_MAP, ERROR_TEMPLATES, type ErrorCode, HtmlDocGenerator, HtmlReporter, InformationDisclosureRule, InjectionRule, InputValidationRule, JsonReporter, JunitReporter, LoggingTransport, MCPClient, type MCPClientInterface, MCPScoreCalculator, MCPSpecError, MarkdownGenerator, NotImplementedError, type OnProtocolMessage, PathTraversalRule, type PayloadSet, type PlatformPayload, ProcessManagerImpl, ProcessRegistry, Profiler, RateLimiter, ResourceExhaustionRule, ResultDiffer, type RunDiff, ScanConfig, type ScanProgress, type ScoreProgress, SecretMasker, type SecurityRule, SecurityScanner, type ServerDocData, TapReporter, type TestDiff, TestExecutor, type TestRunReporter, TestRunner, TestScheduler, WaterfallGenerator, YAML_LIMITS, computeStats, formatError, getPayloadsForMode, getPlatformInfo, getPlatformPayloads, getSafePayloads, loadYamlSafely, queryJsonPath, registerCleanupHandlers, resolveVariables };
547
+ export { AuthBypassRule, BadgeGenerator, BaselineStore, type BenchmarkProgress, BenchmarkRunner, ConnectionManager, ConsoleReporter, DANGEROUS_TOOL_PATTERNS, DocGenerator, type DocGeneratorOptions, type DryRunResult, ERROR_CODE_MAP, ERROR_TEMPLATES, type ErrorCode, HtmlDocGenerator, HtmlReporter, InformationDisclosureRule, InjectionRule, InputValidationRule, JsonReporter, JunitReporter, LoggingTransport, MCPClient, type MCPClientInterface, MCPScoreCalculator, MCPSpecError, MarkdownGenerator, NotImplementedError, type OnProtocolMessage, PathTraversalRule, type PayloadSet, type PlatformPayload, ProcessManagerImpl, ProcessRegistry, Profiler, RateLimiter, ResourceExhaustionRule, ResultDiffer, type RunDiff, ScanConfig, type ScanProgress, type ScoreProgress, SecretMasker, type SecurityRule, SecurityScanner, type ServerDocData, TapReporter, type TestDiff, TestExecutor, type TestRunReporter, TestRunner, TestScheduler, WaterfallGenerator, YAML_LIMITS, computeStats, formatError, getPayloadsForMode, getPlatformInfo, getPlatformPayloads, getSafePayloads, loadYamlSafely, queryJsonPath, registerCleanupHandlers, resolveVariables };
package/dist/index.js CHANGED
@@ -1230,7 +1230,7 @@ var TestExecutor = class {
1230
1230
  assertions: assertionResults
1231
1231
  };
1232
1232
  }
1233
- const response = this.buildResponse(result);
1233
+ const response = this.buildResponse(result, test.rawResponse);
1234
1234
  if (test.assertions) {
1235
1235
  for (const assertion of test.assertions) {
1236
1236
  assertionResults.push(this.runAssertion(assertion, response, Date.now() - startTime));
@@ -1273,7 +1273,7 @@ var TestExecutor = class {
1273
1273
  };
1274
1274
  }
1275
1275
  }
1276
- buildResponse(result) {
1276
+ buildResponse(result, rawResponse) {
1277
1277
  const contents = result.content;
1278
1278
  if (!Array.isArray(contents) || contents.length === 0) {
1279
1279
  return {};
@@ -1281,6 +1281,9 @@ var TestExecutor = class {
1281
1281
  if (contents.length === 1) {
1282
1282
  const item = contents[0];
1283
1283
  if (item["type"] === "text" && typeof item["text"] === "string") {
1284
+ if (rawResponse) {
1285
+ return { content: item["text"], text: item["text"] };
1286
+ }
1284
1287
  try {
1285
1288
  return JSON.parse(item["text"]);
1286
1289
  } catch {
@@ -1420,17 +1423,16 @@ var TestScheduler = class {
1420
1423
  return skippedResults;
1421
1424
  }
1422
1425
  if (parallelism <= 1) {
1423
- const executor2 = new TestExecutor(initialVariables, rateLimiter);
1426
+ const executor = new TestExecutor(initialVariables, rateLimiter);
1424
1427
  const results2 = [];
1425
1428
  for (const test of filteredTests) {
1426
1429
  reporter?.onTestStart(test.name);
1427
- const result = await executor2.execute(test, client);
1430
+ const result = await executor.execute(test, client);
1428
1431
  results2.push(result);
1429
1432
  reporter?.onTestComplete(result);
1430
1433
  }
1431
1434
  return [...results2, ...skippedResults];
1432
1435
  }
1433
- const executor = new TestExecutor(initialVariables, rateLimiter);
1434
1436
  let running = 0;
1435
1437
  const results = new Array(filteredTests.length);
1436
1438
  const waitQueue = [];
@@ -1455,6 +1457,7 @@ var TestScheduler = class {
1455
1457
  return (async () => {
1456
1458
  await acquire();
1457
1459
  try {
1460
+ const executor = new TestExecutor(initialVariables, rateLimiter);
1458
1461
  reporter?.onTestStart(test.name);
1459
1462
  const result = await executor.execute(test, client);
1460
1463
  results[i] = result;
@@ -2086,6 +2089,7 @@ var ACTIVE_RULES = [
2086
2089
  var AGGRESSIVE_RULES = [...ACTIVE_RULES];
2087
2090
  var DEFAULT_TIMEOUT = 1e4;
2088
2091
  var DEFAULT_MAX_PROBES = 50;
2092
+ var DANGEROUS_TOOL_PATTERNS = /^(delete|drop|remove|destroy|kill|purge|truncate|wipe|reset|erase)[_-]|[_-](delete|drop|remove|destroy|kill|purge|truncate|wipe|reset|erase)$/i;
2089
2093
  var ScanConfig = class {
2090
2094
  mode;
2091
2095
  rules;
@@ -2093,12 +2097,16 @@ var ScanConfig = class {
2093
2097
  acknowledgeRisk;
2094
2098
  timeout;
2095
2099
  maxProbesPerTool;
2100
+ excludeTools;
2101
+ dryRun;
2096
2102
  constructor(config = {}) {
2097
2103
  this.mode = config.mode ?? "passive";
2098
2104
  this.severityThreshold = config.severityThreshold ?? "info";
2099
2105
  this.acknowledgeRisk = config.acknowledgeRisk ?? false;
2100
2106
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
2101
2107
  this.maxProbesPerTool = config.maxProbesPerTool ?? DEFAULT_MAX_PROBES;
2108
+ this.excludeTools = config.excludeTools ?? [];
2109
+ this.dryRun = config.dryRun ?? false;
2102
2110
  const allRulesForMode = this.getRulesForMode(this.mode);
2103
2111
  if (config.rules && config.rules.length > 0) {
2104
2112
  this.rules = config.rules.filter((r) => allRulesForMode.includes(r));
@@ -2114,6 +2122,11 @@ var ScanConfig = class {
2114
2122
  const severityIdx = SEVERITY_ORDER.indexOf(severity);
2115
2123
  return severityIdx >= thresholdIdx;
2116
2124
  }
2125
+ isToolExcluded(toolName) {
2126
+ if (this.excludeTools.includes(toolName)) return true;
2127
+ if (this.mode !== "passive" && DANGEROUS_TOOL_PATTERNS.test(toolName)) return true;
2128
+ return false;
2129
+ }
2117
2130
  getRulesForMode(mode) {
2118
2131
  switch (mode) {
2119
2132
  case "passive":
@@ -2690,10 +2703,47 @@ var SecurityScanner = class {
2690
2703
  registerRule(rule) {
2691
2704
  this.rules.set(rule.id, rule);
2692
2705
  }
2706
+ /**
2707
+ * Preview which tools will be scanned without actually running payloads.
2708
+ */
2709
+ async dryRun(client, config) {
2710
+ const allTools = await client.listTools();
2711
+ const toolResults = allTools.map((tool) => {
2712
+ if (config.excludeTools.includes(tool.name)) {
2713
+ return { name: tool.name, included: false, reason: "excluded by --exclude-tools" };
2714
+ }
2715
+ if (config.isToolExcluded(tool.name)) {
2716
+ return { name: tool.name, included: false, reason: "auto-skipped (destructive name)" };
2717
+ }
2718
+ return { name: tool.name, included: true };
2719
+ });
2720
+ return {
2721
+ tools: toolResults,
2722
+ rules: [...config.rules],
2723
+ mode: config.mode
2724
+ };
2725
+ }
2726
+ /**
2727
+ * Filter tools based on config exclusions.
2728
+ */
2729
+ filterTools(tools, config) {
2730
+ return tools.filter((tool) => !config.isToolExcluded(tool.name));
2731
+ }
2693
2732
  async scan(client, config, progress) {
2694
2733
  const startedAt = /* @__PURE__ */ new Date();
2695
2734
  const findings = [];
2696
- const tools = await client.listTools();
2735
+ const allTools = await client.listTools();
2736
+ const tools = this.filterTools(allTools, config);
2737
+ const skippedCount = allTools.length - tools.length;
2738
+ if (skippedCount > 0) {
2739
+ findings.push({
2740
+ id: randomUUID9(),
2741
+ rule: "safety-filter",
2742
+ severity: "info",
2743
+ title: `${skippedCount} tool(s) excluded from scan`,
2744
+ description: `${skippedCount} tool(s) were excluded from scanning due to safety filters or --exclude-tools.`
2745
+ });
2746
+ }
2697
2747
  for (const ruleId of config.rules) {
2698
2748
  const rule = this.rules.get(ruleId);
2699
2749
  if (!rule) continue;
@@ -3121,14 +3171,14 @@ var MCPScoreCalculator = class {
3121
3171
  progress?.onCategoryStart?.("errorHandling");
3122
3172
  const errorHandling = await this.scoreErrorHandling(client, tools);
3123
3173
  progress?.onCategoryComplete?.("errorHandling", errorHandling);
3124
- progress?.onCategoryStart?.("performance");
3125
- const performance4 = await this.scorePerformance(client, tools);
3126
- progress?.onCategoryComplete?.("performance", performance4);
3174
+ progress?.onCategoryStart?.("responsiveness");
3175
+ const responsiveness = await this.scoreResponsiveness(client, tools);
3176
+ progress?.onCategoryComplete?.("responsiveness", responsiveness);
3127
3177
  progress?.onCategoryStart?.("security");
3128
3178
  const security = await this.scoreSecurity(client);
3129
3179
  progress?.onCategoryComplete?.("security", security);
3130
3180
  const overall = Math.round(
3131
- documentation * 0.25 + schemaQuality * 0.25 + errorHandling * 0.2 + performance4 * 0.15 + security * 0.15
3181
+ documentation * 0.25 + schemaQuality * 0.25 + errorHandling * 0.2 + responsiveness * 0.15 + security * 0.15
3132
3182
  );
3133
3183
  return {
3134
3184
  overall,
@@ -3136,7 +3186,7 @@ var MCPScoreCalculator = class {
3136
3186
  documentation,
3137
3187
  schemaQuality,
3138
3188
  errorHandling,
3139
- performance: performance4,
3189
+ responsiveness,
3140
3190
  security
3141
3191
  }
3142
3192
  };
@@ -3172,9 +3222,26 @@ var MCPScoreCalculator = class {
3172
3222
  try {
3173
3223
  const result = await client.callTool(tool.name, {});
3174
3224
  if (result.isError) {
3175
- totalScore += 100;
3225
+ const content = result.content;
3226
+ let isStructured = false;
3227
+ if (Array.isArray(content) && content.length > 0) {
3228
+ isStructured = content.some((c) => {
3229
+ const item = c;
3230
+ const text = item["text"];
3231
+ if (typeof text !== "string") return false;
3232
+ try {
3233
+ const parsed = JSON.parse(text);
3234
+ return typeof parsed === "object" && parsed !== null && ("code" in parsed || "message" in parsed || "error" in parsed);
3235
+ } catch {
3236
+ return false;
3237
+ }
3238
+ });
3239
+ }
3240
+ totalScore += isStructured ? 100 : 80;
3176
3241
  } else {
3177
- totalScore += 50;
3242
+ const schema = tool.inputSchema;
3243
+ const hasRequired = schema && Array.isArray(schema.required) && schema.required.length > 0;
3244
+ totalScore += hasRequired ? 30 : 50;
3178
3245
  }
3179
3246
  } catch {
3180
3247
  totalScore += 0;
@@ -3182,7 +3249,7 @@ var MCPScoreCalculator = class {
3182
3249
  }
3183
3250
  return Math.round(totalScore / testTools.length);
3184
3251
  }
3185
- async scorePerformance(client, tools) {
3252
+ async scoreResponsiveness(client, tools) {
3186
3253
  if (tools.length === 0) return 20;
3187
3254
  const tool = tools[0];
3188
3255
  const latencies = [];
@@ -3260,6 +3327,7 @@ export {
3260
3327
  BenchmarkRunner,
3261
3328
  ConnectionManager,
3262
3329
  ConsoleReporter,
3330
+ DANGEROUS_TOOL_PATTERNS,
3263
3331
  DocGenerator,
3264
3332
  ERROR_CODE_MAP,
3265
3333
  ERROR_TEMPLATES,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpspec/core",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  "expr-eval": "^2.0.2",
32
32
  "handlebars": "^4.7.8",
33
33
  "zod": "^3.22.0",
34
- "@mcpspec/shared": "1.0.2"
34
+ "@mcpspec/shared": "1.0.3"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/js-yaml": "^4.0.9",