@mcpspec/core 1.1.0 → 1.2.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
@@ -1,6 +1,6 @@
1
1
  # @mcpspec/core
2
2
 
3
- Core engine for [MCPSpec](https://www.npmjs.com/package/mcpspec) — MCP client, test runner, security scanner, performance profiler, documentation generator, and quality scorer.
3
+ Core engine for [MCPSpec](https://www.npmjs.com/package/mcpspec) — MCP client, test runner, security scanner, performance profiler, documentation generator, quality scorer, and mock server generator.
4
4
 
5
5
  > **For CLI usage, install [`mcpspec`](https://www.npmjs.com/package/mcpspec) instead.** This package is for programmatic use — embedding MCPSpec capabilities in your own tools.
6
6
 
@@ -114,6 +114,12 @@ Evaluated via `TestExecutor` — schema, equals, contains, exists, matches, type
114
114
  - `RecordingReplayer` — Replay recorded steps against a live server
115
115
  - `RecordingDiffer` — Diff original recording vs replayed results (matched/changed/added/removed)
116
116
 
117
+ ### Mock Server
118
+
119
+ - `MockMCPServer` — Start a mock MCP server from a recording (stdio transport, drop-in replacement)
120
+ - `ResponseMatcher` — Match incoming tool calls to recorded responses (`match` or `sequential` mode)
121
+ - `MockGenerator` — Generate standalone `.js` mock server files (only requires `@modelcontextprotocol/sdk`)
122
+
117
123
  ### Utilities
118
124
 
119
125
  - `loadYamlSafely` — FAILSAFE_SCHEMA YAML parsing
package/dist/index.d.ts CHANGED
@@ -591,4 +591,70 @@ declare class RecordingDiffer {
591
591
  private describeChange;
592
592
  }
593
593
 
594
- export { AuthBypassRule, BadgeGenerator, BaselineStore, type BenchmarkProgress, BenchmarkRunner, ConnectionManager, ConsoleReporter, DANGEROUS_TOOL_PATTERNS, DocGenerator, type DocGeneratorOptions, type DryRunResult, ERROR_CODE_MAP, ERROR_TEMPLATES, type ErrorCode, ExcessiveAgencyRule, 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, RecordingDiffer, RecordingReplayer, RecordingStore, type ReplayProgress, type ReplayResult, ResourceExhaustionRule, ResultDiffer, type RunDiff, ScanConfig, type ScanProgress, type ScoreProgress, SecretMasker, type SecurityRule, SecurityScanner, type ServerDocData, TapReporter, type TestDiff, TestExecutor, type TestRunReporter, TestRunner, TestScheduler, ToolPoisoningRule, WaterfallGenerator, YAML_LIMITS, computeStats, formatError, getPayloadsForMode, getPlatformInfo, getPlatformPayloads, getSafePayloads, loadYamlSafely, queryJsonPath, registerCleanupHandlers, resolveVariables };
594
+ type MatchMode = 'match' | 'sequential';
595
+ type OnMissingBehavior = 'error' | 'empty';
596
+ interface ResponseMatcherConfig {
597
+ mode: MatchMode;
598
+ onMissing: OnMissingBehavior;
599
+ }
600
+ interface MatchResult {
601
+ output: unknown[];
602
+ isError: boolean;
603
+ durationMs: number;
604
+ }
605
+ interface MatcherStats {
606
+ totalSteps: number;
607
+ servedCount: number;
608
+ remainingCount: number;
609
+ }
610
+ /**
611
+ * Matches incoming tool calls to recorded responses.
612
+ *
613
+ * - `match` mode: tries exact input match first, then falls back to next queued response for that tool.
614
+ * - `sequential` mode: serves responses in recorded order regardless of tool name/input.
615
+ */
616
+ declare class ResponseMatcher {
617
+ private readonly config;
618
+ private readonly steps;
619
+ private servedCount;
620
+ private toolQueues;
621
+ private sequentialCursor;
622
+ constructor(steps: RecordingStep[], config: ResponseMatcherConfig);
623
+ match(toolName: string, input: Record<string, unknown>): MatchResult | null;
624
+ getStats(): MatcherStats;
625
+ private matchSequential;
626
+ private matchByTool;
627
+ private normalizeInput;
628
+ private stepToResult;
629
+ }
630
+
631
+ interface MockServerConfig {
632
+ recording: Recording;
633
+ mode: MatchMode;
634
+ latency: number | 'original';
635
+ onMissing: OnMissingBehavior;
636
+ }
637
+ interface MockServerStats extends MatcherStats {
638
+ toolCount: number;
639
+ }
640
+ declare class MockMCPServer {
641
+ private readonly config;
642
+ private readonly matcher;
643
+ private readonly server;
644
+ constructor(config: MockServerConfig);
645
+ start(transport?: Transport): Promise<void>;
646
+ getStats(): MockServerStats;
647
+ private registerHandlers;
648
+ }
649
+
650
+ interface MockGeneratorOptions {
651
+ recording: Recording;
652
+ mode: MatchMode;
653
+ latency: number | 'original';
654
+ onMissing: OnMissingBehavior;
655
+ }
656
+ declare class MockGenerator {
657
+ generate(options: MockGeneratorOptions): string;
658
+ }
659
+
660
+ export { AuthBypassRule, BadgeGenerator, BaselineStore, type BenchmarkProgress, BenchmarkRunner, ConnectionManager, ConsoleReporter, DANGEROUS_TOOL_PATTERNS, DocGenerator, type DocGeneratorOptions, type DryRunResult, ERROR_CODE_MAP, ERROR_TEMPLATES, type ErrorCode, ExcessiveAgencyRule, HtmlDocGenerator, HtmlReporter, InformationDisclosureRule, InjectionRule, InputValidationRule, JsonReporter, JunitReporter, LoggingTransport, MCPClient, type MCPClientInterface, MCPScoreCalculator, MCPSpecError, MarkdownGenerator, type MatchMode, type MatchResult, type MatcherStats, MockGenerator, type MockGeneratorOptions, MockMCPServer, type MockServerConfig, type MockServerStats, NotImplementedError, type OnMissingBehavior, type OnProtocolMessage, PathTraversalRule, type PayloadSet, type PlatformPayload, ProcessManagerImpl, ProcessRegistry, Profiler, RateLimiter, RecordingDiffer, RecordingReplayer, RecordingStore, type ReplayProgress, type ReplayResult, ResourceExhaustionRule, ResponseMatcher, type ResponseMatcherConfig, ResultDiffer, type RunDiff, ScanConfig, type ScanProgress, type ScoreProgress, SecretMasker, type SecurityRule, SecurityScanner, type ServerDocData, TapReporter, type TestDiff, TestExecutor, type TestRunReporter, TestRunner, TestScheduler, ToolPoisoningRule, WaterfallGenerator, YAML_LIMITS, computeStats, formatError, getPayloadsForMode, getPlatformInfo, getPlatformPayloads, getSafePayloads, loadYamlSafely, queryJsonPath, registerCleanupHandlers, resolveVariables };
package/dist/index.js CHANGED
@@ -3708,6 +3708,310 @@ var RecordingDiffer = class {
3708
3708
  return parts.join("; ");
3709
3709
  }
3710
3710
  };
3711
+
3712
+ // src/mock/mock-server.ts
3713
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3714
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3715
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3716
+
3717
+ // src/mock/response-matcher.ts
3718
+ function stableStringify(obj) {
3719
+ if (obj === null || typeof obj !== "object") {
3720
+ return JSON.stringify(obj);
3721
+ }
3722
+ if (Array.isArray(obj)) {
3723
+ return "[" + obj.map(stableStringify).join(",") + "]";
3724
+ }
3725
+ const keys = Object.keys(obj).sort();
3726
+ const parts = keys.map((key) => {
3727
+ const val = obj[key];
3728
+ return JSON.stringify(key) + ":" + stableStringify(val);
3729
+ });
3730
+ return "{" + parts.join(",") + "}";
3731
+ }
3732
+ var ResponseMatcher = class {
3733
+ config;
3734
+ steps;
3735
+ servedCount = 0;
3736
+ // match mode: per-tool queues
3737
+ toolQueues = /* @__PURE__ */ new Map();
3738
+ // sequential mode: single cursor
3739
+ sequentialCursor = 0;
3740
+ constructor(steps, config) {
3741
+ this.steps = steps;
3742
+ this.config = config;
3743
+ if (config.mode === "match") {
3744
+ for (const step of steps) {
3745
+ const queue = this.toolQueues.get(step.tool);
3746
+ if (queue) {
3747
+ queue.push(step);
3748
+ } else {
3749
+ this.toolQueues.set(step.tool, [step]);
3750
+ }
3751
+ }
3752
+ }
3753
+ }
3754
+ match(toolName, input) {
3755
+ if (this.config.mode === "sequential") {
3756
+ return this.matchSequential();
3757
+ }
3758
+ return this.matchByTool(toolName, input);
3759
+ }
3760
+ getStats() {
3761
+ return {
3762
+ totalSteps: this.steps.length,
3763
+ servedCount: this.servedCount,
3764
+ remainingCount: this.steps.length - this.servedCount
3765
+ };
3766
+ }
3767
+ matchSequential() {
3768
+ if (this.sequentialCursor >= this.steps.length) {
3769
+ return null;
3770
+ }
3771
+ const step = this.steps[this.sequentialCursor];
3772
+ this.sequentialCursor++;
3773
+ this.servedCount++;
3774
+ return this.stepToResult(step);
3775
+ }
3776
+ matchByTool(toolName, input) {
3777
+ const queue = this.toolQueues.get(toolName);
3778
+ if (!queue || queue.length === 0) {
3779
+ return null;
3780
+ }
3781
+ const inputKey = this.normalizeInput(input);
3782
+ const exactIndex = queue.findIndex((s) => this.normalizeInput(s.input) === inputKey);
3783
+ if (exactIndex !== -1) {
3784
+ const step2 = queue.splice(exactIndex, 1)[0];
3785
+ this.servedCount++;
3786
+ return this.stepToResult(step2);
3787
+ }
3788
+ const step = queue.shift();
3789
+ this.servedCount++;
3790
+ return this.stepToResult(step);
3791
+ }
3792
+ normalizeInput(input) {
3793
+ return stableStringify(input);
3794
+ }
3795
+ stepToResult(step) {
3796
+ return {
3797
+ output: step.output,
3798
+ isError: step.isError === true,
3799
+ durationMs: step.durationMs ?? 0
3800
+ };
3801
+ }
3802
+ };
3803
+
3804
+ // src/mock/mock-server.ts
3805
+ var MockMCPServer = class {
3806
+ config;
3807
+ matcher;
3808
+ server;
3809
+ constructor(config) {
3810
+ this.config = config;
3811
+ this.matcher = new ResponseMatcher(config.recording.steps, {
3812
+ mode: config.mode,
3813
+ onMissing: config.onMissing
3814
+ });
3815
+ this.server = new Server(
3816
+ {
3817
+ name: config.recording.serverName ?? config.recording.name,
3818
+ version: "1.0.0-mock"
3819
+ },
3820
+ {
3821
+ capabilities: {
3822
+ tools: {}
3823
+ }
3824
+ }
3825
+ );
3826
+ this.registerHandlers();
3827
+ }
3828
+ async start(transport) {
3829
+ const t = transport ?? new StdioServerTransport();
3830
+ await this.server.connect(t);
3831
+ process.stderr.write(`Mock server started (${this.config.recording.steps.length} recorded steps)
3832
+ `);
3833
+ }
3834
+ getStats() {
3835
+ return {
3836
+ ...this.matcher.getStats(),
3837
+ toolCount: this.config.recording.tools.length
3838
+ };
3839
+ }
3840
+ registerHandlers() {
3841
+ const tools = this.config.recording.tools;
3842
+ const matcher = this.matcher;
3843
+ const config = this.config;
3844
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
3845
+ return {
3846
+ tools: tools.map((t) => ({
3847
+ name: t.name,
3848
+ description: t.description ?? "",
3849
+ inputSchema: { type: "object", properties: {} }
3850
+ }))
3851
+ };
3852
+ });
3853
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
3854
+ const toolName = request.params.name;
3855
+ const input = request.params.arguments ?? {};
3856
+ const result = matcher.match(toolName, input);
3857
+ if (!result) {
3858
+ if (config.onMissing === "empty") {
3859
+ return {
3860
+ content: [{ type: "text", text: "" }],
3861
+ isError: false
3862
+ };
3863
+ }
3864
+ return {
3865
+ content: [{ type: "text", text: `No recorded response for tool "${toolName}"` }],
3866
+ isError: true
3867
+ };
3868
+ }
3869
+ const delay = config.latency === "original" ? result.durationMs : config.latency;
3870
+ if (delay > 0) {
3871
+ await new Promise((resolve) => setTimeout(resolve, delay));
3872
+ }
3873
+ return {
3874
+ content: result.output,
3875
+ isError: result.isError
3876
+ };
3877
+ });
3878
+ }
3879
+ };
3880
+
3881
+ // src/mock/mock-generator.ts
3882
+ var MockGenerator = class {
3883
+ generate(options) {
3884
+ const recordingJson = JSON.stringify(options.recording, null, 2);
3885
+ const latencyValue = options.latency === "original" ? `'original'` : String(options.latency);
3886
+ return `#!/usr/bin/env node
3887
+ // Auto-generated mock MCP server by mcpspec
3888
+ // Recording: ${options.recording.name}
3889
+ // Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
3890
+ //
3891
+ // Dependencies: @modelcontextprotocol/sdk
3892
+ // Install: npm install @modelcontextprotocol/sdk
3893
+ // Run: node ${options.recording.name}-mock.js
3894
+
3895
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3896
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3897
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3898
+
3899
+ const RECORDING = ${recordingJson};
3900
+
3901
+ const MODE = '${options.mode}';
3902
+ const LATENCY = ${latencyValue};
3903
+ const ON_MISSING = '${options.onMissing}';
3904
+
3905
+ // --- Stable stringify (deep key sorting) ---
3906
+
3907
+ function stableStringify(obj) {
3908
+ if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
3909
+ if (Array.isArray(obj)) return '[' + obj.map(stableStringify).join(',') + ']';
3910
+ const keys = Object.keys(obj).sort();
3911
+ const parts = keys.map((k) => JSON.stringify(k) + ':' + stableStringify(obj[k]));
3912
+ return '{' + parts.join(',') + '}';
3913
+ }
3914
+
3915
+ // --- ResponseMatcher (inlined) ---
3916
+
3917
+ class ResponseMatcher {
3918
+ constructor(steps, config) {
3919
+ this.config = config;
3920
+ this.steps = steps;
3921
+ this.servedCount = 0;
3922
+ this.toolQueues = new Map();
3923
+ this.sequentialCursor = 0;
3924
+
3925
+ if (config.mode === 'match') {
3926
+ for (const step of steps) {
3927
+ const queue = this.toolQueues.get(step.tool);
3928
+ if (queue) {
3929
+ queue.push(step);
3930
+ } else {
3931
+ this.toolQueues.set(step.tool, [step]);
3932
+ }
3933
+ }
3934
+ }
3935
+ }
3936
+
3937
+ match(toolName, input) {
3938
+ if (this.config.mode === 'sequential') {
3939
+ return this._matchSequential();
3940
+ }
3941
+ return this._matchByTool(toolName, input);
3942
+ }
3943
+
3944
+ _matchSequential() {
3945
+ if (this.sequentialCursor >= this.steps.length) return null;
3946
+ const step = this.steps[this.sequentialCursor];
3947
+ this.sequentialCursor++;
3948
+ this.servedCount++;
3949
+ return { output: step.output, isError: step.isError === true, durationMs: step.durationMs || 0 };
3950
+ }
3951
+
3952
+ _matchByTool(toolName, input) {
3953
+ const queue = this.toolQueues.get(toolName);
3954
+ if (!queue || queue.length === 0) return null;
3955
+
3956
+ const inputKey = stableStringify(input);
3957
+ const exactIndex = queue.findIndex(
3958
+ (s) => stableStringify(s.input) === inputKey
3959
+ );
3960
+
3961
+ let step;
3962
+ if (exactIndex !== -1) {
3963
+ step = queue.splice(exactIndex, 1)[0];
3964
+ } else {
3965
+ step = queue.shift();
3966
+ }
3967
+ this.servedCount++;
3968
+ return { output: step.output, isError: step.isError === true, durationMs: step.durationMs || 0 };
3969
+ }
3970
+ }
3971
+
3972
+ // --- Server setup ---
3973
+
3974
+ const matcher = new ResponseMatcher(RECORDING.steps, { mode: MODE, onMissing: ON_MISSING });
3975
+
3976
+ const server = new Server(
3977
+ { name: RECORDING.serverName || RECORDING.name, version: '1.0.0-mock' },
3978
+ { capabilities: { tools: {} } }
3979
+ );
3980
+
3981
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3982
+ tools: RECORDING.tools.map((t) => ({
3983
+ name: t.name,
3984
+ description: t.description || '',
3985
+ inputSchema: { type: 'object', properties: {} },
3986
+ })),
3987
+ }));
3988
+
3989
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3990
+ const toolName = request.params.name;
3991
+ const input = request.params.arguments || {};
3992
+ const result = matcher.match(toolName, input);
3993
+
3994
+ if (!result) {
3995
+ if (ON_MISSING === 'empty') {
3996
+ return { content: [{ type: 'text', text: '' }], isError: false };
3997
+ }
3998
+ return { content: [{ type: 'text', text: \`No recorded response for tool "\${toolName}"\` }], isError: true };
3999
+ }
4000
+
4001
+ const delay = LATENCY === 'original' ? result.durationMs : LATENCY;
4002
+ if (delay > 0) {
4003
+ await new Promise((resolve) => setTimeout(resolve, delay));
4004
+ }
4005
+
4006
+ return { content: result.output, isError: result.isError };
4007
+ });
4008
+
4009
+ const transport = new StdioServerTransport();
4010
+ await server.connect(transport);
4011
+ process.stderr.write(\`Mock server started (\${RECORDING.steps.length} recorded steps)\\n\`);
4012
+ `;
4013
+ }
4014
+ };
3711
4015
  export {
3712
4016
  AuthBypassRule,
3713
4017
  BadgeGenerator,
@@ -3732,6 +4036,8 @@ export {
3732
4036
  MCPScoreCalculator,
3733
4037
  MCPSpecError,
3734
4038
  MarkdownGenerator,
4039
+ MockGenerator,
4040
+ MockMCPServer,
3735
4041
  NotImplementedError,
3736
4042
  PathTraversalRule,
3737
4043
  ProcessManagerImpl,
@@ -3742,6 +4048,7 @@ export {
3742
4048
  RecordingReplayer,
3743
4049
  RecordingStore,
3744
4050
  ResourceExhaustionRule,
4051
+ ResponseMatcher,
3745
4052
  ResultDiffer,
3746
4053
  ScanConfig,
3747
4054
  SecretMasker,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcpspec/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
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.1.0"
34
+ "@mcpspec/shared": "1.2.1"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/js-yaml": "^4.0.9",