@mcpspec/core 1.1.0 → 1.2.0
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 +7 -1
- package/dist/index.d.ts +67 -1
- package/dist/index.js +283 -0
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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,286 @@ 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
|
+
var ResponseMatcher = class {
|
|
3719
|
+
config;
|
|
3720
|
+
steps;
|
|
3721
|
+
servedCount = 0;
|
|
3722
|
+
// match mode: per-tool queues
|
|
3723
|
+
toolQueues = /* @__PURE__ */ new Map();
|
|
3724
|
+
// sequential mode: single cursor
|
|
3725
|
+
sequentialCursor = 0;
|
|
3726
|
+
constructor(steps, config) {
|
|
3727
|
+
this.steps = steps;
|
|
3728
|
+
this.config = config;
|
|
3729
|
+
if (config.mode === "match") {
|
|
3730
|
+
for (const step of steps) {
|
|
3731
|
+
const queue = this.toolQueues.get(step.tool);
|
|
3732
|
+
if (queue) {
|
|
3733
|
+
queue.push(step);
|
|
3734
|
+
} else {
|
|
3735
|
+
this.toolQueues.set(step.tool, [step]);
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
match(toolName, input) {
|
|
3741
|
+
if (this.config.mode === "sequential") {
|
|
3742
|
+
return this.matchSequential();
|
|
3743
|
+
}
|
|
3744
|
+
return this.matchByTool(toolName, input);
|
|
3745
|
+
}
|
|
3746
|
+
getStats() {
|
|
3747
|
+
return {
|
|
3748
|
+
totalSteps: this.steps.length,
|
|
3749
|
+
servedCount: this.servedCount,
|
|
3750
|
+
remainingCount: this.steps.length - this.servedCount
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
3753
|
+
matchSequential() {
|
|
3754
|
+
if (this.sequentialCursor >= this.steps.length) {
|
|
3755
|
+
return null;
|
|
3756
|
+
}
|
|
3757
|
+
const step = this.steps[this.sequentialCursor];
|
|
3758
|
+
this.sequentialCursor++;
|
|
3759
|
+
this.servedCount++;
|
|
3760
|
+
return this.stepToResult(step);
|
|
3761
|
+
}
|
|
3762
|
+
matchByTool(toolName, input) {
|
|
3763
|
+
const queue = this.toolQueues.get(toolName);
|
|
3764
|
+
if (!queue || queue.length === 0) {
|
|
3765
|
+
return null;
|
|
3766
|
+
}
|
|
3767
|
+
const inputKey = this.normalizeInput(input);
|
|
3768
|
+
const exactIndex = queue.findIndex((s) => this.normalizeInput(s.input) === inputKey);
|
|
3769
|
+
if (exactIndex !== -1) {
|
|
3770
|
+
const step2 = queue.splice(exactIndex, 1)[0];
|
|
3771
|
+
this.servedCount++;
|
|
3772
|
+
return this.stepToResult(step2);
|
|
3773
|
+
}
|
|
3774
|
+
const step = queue.shift();
|
|
3775
|
+
this.servedCount++;
|
|
3776
|
+
return this.stepToResult(step);
|
|
3777
|
+
}
|
|
3778
|
+
normalizeInput(input) {
|
|
3779
|
+
return JSON.stringify(input, Object.keys(input).sort());
|
|
3780
|
+
}
|
|
3781
|
+
stepToResult(step) {
|
|
3782
|
+
return {
|
|
3783
|
+
output: step.output,
|
|
3784
|
+
isError: step.isError === true,
|
|
3785
|
+
durationMs: step.durationMs ?? 0
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
};
|
|
3789
|
+
|
|
3790
|
+
// src/mock/mock-server.ts
|
|
3791
|
+
var MockMCPServer = class {
|
|
3792
|
+
config;
|
|
3793
|
+
matcher;
|
|
3794
|
+
server;
|
|
3795
|
+
constructor(config) {
|
|
3796
|
+
this.config = config;
|
|
3797
|
+
this.matcher = new ResponseMatcher(config.recording.steps, {
|
|
3798
|
+
mode: config.mode,
|
|
3799
|
+
onMissing: config.onMissing
|
|
3800
|
+
});
|
|
3801
|
+
this.server = new Server(
|
|
3802
|
+
{
|
|
3803
|
+
name: config.recording.serverName ?? config.recording.name,
|
|
3804
|
+
version: "1.0.0-mock"
|
|
3805
|
+
},
|
|
3806
|
+
{
|
|
3807
|
+
capabilities: {
|
|
3808
|
+
tools: {}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
);
|
|
3812
|
+
this.registerHandlers();
|
|
3813
|
+
}
|
|
3814
|
+
async start(transport) {
|
|
3815
|
+
const t = transport ?? new StdioServerTransport();
|
|
3816
|
+
await this.server.connect(t);
|
|
3817
|
+
process.stderr.write(`Mock server started (${this.config.recording.steps.length} recorded steps)
|
|
3818
|
+
`);
|
|
3819
|
+
}
|
|
3820
|
+
getStats() {
|
|
3821
|
+
return {
|
|
3822
|
+
...this.matcher.getStats(),
|
|
3823
|
+
toolCount: this.config.recording.tools.length
|
|
3824
|
+
};
|
|
3825
|
+
}
|
|
3826
|
+
registerHandlers() {
|
|
3827
|
+
const tools = this.config.recording.tools;
|
|
3828
|
+
const matcher = this.matcher;
|
|
3829
|
+
const config = this.config;
|
|
3830
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
3831
|
+
return {
|
|
3832
|
+
tools: tools.map((t) => ({
|
|
3833
|
+
name: t.name,
|
|
3834
|
+
description: t.description ?? "",
|
|
3835
|
+
inputSchema: { type: "object", properties: {} }
|
|
3836
|
+
}))
|
|
3837
|
+
};
|
|
3838
|
+
});
|
|
3839
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3840
|
+
const toolName = request.params.name;
|
|
3841
|
+
const input = request.params.arguments ?? {};
|
|
3842
|
+
const result = matcher.match(toolName, input);
|
|
3843
|
+
if (!result) {
|
|
3844
|
+
if (config.onMissing === "empty") {
|
|
3845
|
+
return {
|
|
3846
|
+
content: [{ type: "text", text: "" }],
|
|
3847
|
+
isError: false
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
return {
|
|
3851
|
+
content: [{ type: "text", text: `No recorded response for tool "${toolName}"` }],
|
|
3852
|
+
isError: true
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
const delay = config.latency === "original" ? result.durationMs : config.latency;
|
|
3856
|
+
if (delay > 0) {
|
|
3857
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3858
|
+
}
|
|
3859
|
+
return {
|
|
3860
|
+
content: result.output,
|
|
3861
|
+
isError: result.isError
|
|
3862
|
+
};
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
};
|
|
3866
|
+
|
|
3867
|
+
// src/mock/mock-generator.ts
|
|
3868
|
+
var MockGenerator = class {
|
|
3869
|
+
generate(options) {
|
|
3870
|
+
const recordingJson = JSON.stringify(options.recording, null, 2);
|
|
3871
|
+
const latencyValue = options.latency === "original" ? `'original'` : String(options.latency);
|
|
3872
|
+
return `#!/usr/bin/env node
|
|
3873
|
+
// Auto-generated mock MCP server by mcpspec
|
|
3874
|
+
// Recording: ${options.recording.name}
|
|
3875
|
+
// Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
3876
|
+
//
|
|
3877
|
+
// Dependencies: @modelcontextprotocol/sdk
|
|
3878
|
+
// Install: npm install @modelcontextprotocol/sdk
|
|
3879
|
+
// Run: node ${options.recording.name}-mock.js
|
|
3880
|
+
|
|
3881
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3882
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3883
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
3884
|
+
|
|
3885
|
+
const RECORDING = ${recordingJson};
|
|
3886
|
+
|
|
3887
|
+
const MODE = '${options.mode}';
|
|
3888
|
+
const LATENCY = ${latencyValue};
|
|
3889
|
+
const ON_MISSING = '${options.onMissing}';
|
|
3890
|
+
|
|
3891
|
+
// --- ResponseMatcher (inlined) ---
|
|
3892
|
+
|
|
3893
|
+
class ResponseMatcher {
|
|
3894
|
+
constructor(steps, config) {
|
|
3895
|
+
this.config = config;
|
|
3896
|
+
this.steps = steps;
|
|
3897
|
+
this.servedCount = 0;
|
|
3898
|
+
this.toolQueues = new Map();
|
|
3899
|
+
this.sequentialCursor = 0;
|
|
3900
|
+
|
|
3901
|
+
if (config.mode === 'match') {
|
|
3902
|
+
for (const step of steps) {
|
|
3903
|
+
const queue = this.toolQueues.get(step.tool);
|
|
3904
|
+
if (queue) {
|
|
3905
|
+
queue.push(step);
|
|
3906
|
+
} else {
|
|
3907
|
+
this.toolQueues.set(step.tool, [step]);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
match(toolName, input) {
|
|
3914
|
+
if (this.config.mode === 'sequential') {
|
|
3915
|
+
return this._matchSequential();
|
|
3916
|
+
}
|
|
3917
|
+
return this._matchByTool(toolName, input);
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
_matchSequential() {
|
|
3921
|
+
if (this.sequentialCursor >= this.steps.length) return null;
|
|
3922
|
+
const step = this.steps[this.sequentialCursor];
|
|
3923
|
+
this.sequentialCursor++;
|
|
3924
|
+
this.servedCount++;
|
|
3925
|
+
return { output: step.output, isError: step.isError === true, durationMs: step.durationMs || 0 };
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
_matchByTool(toolName, input) {
|
|
3929
|
+
const queue = this.toolQueues.get(toolName);
|
|
3930
|
+
if (!queue || queue.length === 0) return null;
|
|
3931
|
+
|
|
3932
|
+
const inputKey = JSON.stringify(input, Object.keys(input).sort());
|
|
3933
|
+
const exactIndex = queue.findIndex(
|
|
3934
|
+
(s) => JSON.stringify(s.input, Object.keys(s.input).sort()) === inputKey
|
|
3935
|
+
);
|
|
3936
|
+
|
|
3937
|
+
let step;
|
|
3938
|
+
if (exactIndex !== -1) {
|
|
3939
|
+
step = queue.splice(exactIndex, 1)[0];
|
|
3940
|
+
} else {
|
|
3941
|
+
step = queue.shift();
|
|
3942
|
+
}
|
|
3943
|
+
this.servedCount++;
|
|
3944
|
+
return { output: step.output, isError: step.isError === true, durationMs: step.durationMs || 0 };
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3948
|
+
// --- Server setup ---
|
|
3949
|
+
|
|
3950
|
+
const matcher = new ResponseMatcher(RECORDING.steps, { mode: MODE, onMissing: ON_MISSING });
|
|
3951
|
+
|
|
3952
|
+
const server = new Server(
|
|
3953
|
+
{ name: RECORDING.serverName || RECORDING.name, version: '1.0.0-mock' },
|
|
3954
|
+
{ capabilities: { tools: {} } }
|
|
3955
|
+
);
|
|
3956
|
+
|
|
3957
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3958
|
+
tools: RECORDING.tools.map((t) => ({
|
|
3959
|
+
name: t.name,
|
|
3960
|
+
description: t.description || '',
|
|
3961
|
+
inputSchema: { type: 'object', properties: {} },
|
|
3962
|
+
})),
|
|
3963
|
+
}));
|
|
3964
|
+
|
|
3965
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
3966
|
+
const toolName = request.params.name;
|
|
3967
|
+
const input = request.params.arguments || {};
|
|
3968
|
+
const result = matcher.match(toolName, input);
|
|
3969
|
+
|
|
3970
|
+
if (!result) {
|
|
3971
|
+
if (ON_MISSING === 'empty') {
|
|
3972
|
+
return { content: [{ type: 'text', text: '' }], isError: false };
|
|
3973
|
+
}
|
|
3974
|
+
return { content: [{ type: 'text', text: \`No recorded response for tool "\${toolName}"\` }], isError: true };
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
const delay = LATENCY === 'original' ? result.durationMs : LATENCY;
|
|
3978
|
+
if (delay > 0) {
|
|
3979
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
return { content: result.output, isError: result.isError };
|
|
3983
|
+
});
|
|
3984
|
+
|
|
3985
|
+
const transport = new StdioServerTransport();
|
|
3986
|
+
await server.connect(transport);
|
|
3987
|
+
process.stderr.write(\`Mock server started (\${RECORDING.steps.length} recorded steps)\\n\`);
|
|
3988
|
+
`;
|
|
3989
|
+
}
|
|
3990
|
+
};
|
|
3711
3991
|
export {
|
|
3712
3992
|
AuthBypassRule,
|
|
3713
3993
|
BadgeGenerator,
|
|
@@ -3732,6 +4012,8 @@ export {
|
|
|
3732
4012
|
MCPScoreCalculator,
|
|
3733
4013
|
MCPSpecError,
|
|
3734
4014
|
MarkdownGenerator,
|
|
4015
|
+
MockGenerator,
|
|
4016
|
+
MockMCPServer,
|
|
3735
4017
|
NotImplementedError,
|
|
3736
4018
|
PathTraversalRule,
|
|
3737
4019
|
ProcessManagerImpl,
|
|
@@ -3742,6 +4024,7 @@ export {
|
|
|
3742
4024
|
RecordingReplayer,
|
|
3743
4025
|
RecordingStore,
|
|
3744
4026
|
ResourceExhaustionRule,
|
|
4027
|
+
ResponseMatcher,
|
|
3745
4028
|
ResultDiffer,
|
|
3746
4029
|
ScanConfig,
|
|
3747
4030
|
SecretMasker,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcpspec/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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.
|
|
34
|
+
"@mcpspec/shared": "1.2.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/js-yaml": "^4.0.9",
|