@lunatest/mcp 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin/mcp-stdio.d.ts +2 -0
  3. package/dist/bin/mcp-stdio.js +16 -0
  4. package/dist/generation/combinatorial.d.ts +2 -0
  5. package/dist/generation/combinatorial.js +37 -0
  6. package/dist/generation/mutator.d.ts +5 -0
  7. package/dist/generation/mutator.js +51 -0
  8. package/dist/index.d.ts +10 -0
  9. package/dist/index.js +10 -0
  10. package/dist/prompts/analyze-failure.d.ts +1 -0
  11. package/dist/prompts/analyze-failure.js +3 -0
  12. package/dist/prompts/generate-edge-cases.d.ts +1 -0
  13. package/dist/prompts/generate-edge-cases.js +3 -0
  14. package/dist/prompts/improve-coverage.d.ts +1 -0
  15. package/dist/prompts/improve-coverage.js +3 -0
  16. package/dist/prompts/index.d.ts +5 -0
  17. package/dist/prompts/index.js +24 -0
  18. package/dist/prompts/regression-from-diff.d.ts +1 -0
  19. package/dist/prompts/regression-from-diff.js +3 -0
  20. package/dist/resources/components.d.ts +4 -0
  21. package/dist/resources/components.js +6 -0
  22. package/dist/resources/coverage.d.ts +4 -0
  23. package/dist/resources/coverage.js +6 -0
  24. package/dist/resources/guide.d.ts +7 -0
  25. package/dist/resources/guide.js +13 -0
  26. package/dist/resources/index.d.ts +11 -0
  27. package/dist/resources/index.js +16 -0
  28. package/dist/resources/mocks.d.ts +10 -0
  29. package/dist/resources/mocks.js +12 -0
  30. package/dist/resources/protocols.d.ts +4 -0
  31. package/dist/resources/protocols.js +6 -0
  32. package/dist/resources/scenarios.d.ts +5 -0
  33. package/dist/resources/scenarios.js +6 -0
  34. package/dist/server.d.ts +36 -0
  35. package/dist/server.js +205 -0
  36. package/dist/tools/component.d.ts +10 -0
  37. package/dist/tools/component.js +10 -0
  38. package/dist/tools/coverage.d.ts +16 -0
  39. package/dist/tools/coverage.js +29 -0
  40. package/dist/tools/mock.d.ts +8 -0
  41. package/dist/tools/mock.js +29 -0
  42. package/dist/tools/scenario.d.ts +44 -0
  43. package/dist/tools/scenario.js +96 -0
  44. package/dist/transport/stdio.d.ts +24 -0
  45. package/dist/transport/stdio.js +63 -0
  46. package/package.json +35 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coco
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import { createMcpServer } from "../server.js";
3
+ import { runStdioServer } from "../transport/stdio.js";
4
+ const server = createMcpServer({
5
+ scenarios: [],
6
+ });
7
+ runStdioServer({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ error: process.stderr,
11
+ server,
12
+ }).catch((error) => {
13
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
14
+ process.stderr.write(`${message}\n`);
15
+ process.exitCode = 1;
16
+ });
@@ -0,0 +1,2 @@
1
+ export type CombinationSpace = Record<string, unknown[]>;
2
+ export declare function generate(space: CombinationSpace, limit?: number): Array<Record<string, unknown>>;
@@ -0,0 +1,37 @@
1
+ export function generate(space, limit = 256) {
2
+ const keys = Object.keys(space);
3
+ if (keys.length === 0) {
4
+ return [];
5
+ }
6
+ const combos = [];
7
+ const visit = (depth, acc) => {
8
+ if (combos.length >= limit) {
9
+ return;
10
+ }
11
+ if (depth === keys.length) {
12
+ combos.push({ ...acc });
13
+ return;
14
+ }
15
+ const key = keys[depth];
16
+ if (!key) {
17
+ return;
18
+ }
19
+ const values = space[key] ?? [];
20
+ if (values.length === 0) {
21
+ acc[key] = undefined;
22
+ visit(depth + 1, acc);
23
+ delete acc[key];
24
+ return;
25
+ }
26
+ for (const value of values) {
27
+ if (combos.length >= limit) {
28
+ return;
29
+ }
30
+ acc[key] = value;
31
+ visit(depth + 1, acc);
32
+ }
33
+ delete acc[key];
34
+ };
35
+ visit(0, {});
36
+ return combos;
37
+ }
@@ -0,0 +1,5 @@
1
+ import type { ScenarioDescriptor } from "../tools/scenario.js";
2
+ export declare function mutateValues(lua: string, variantIndex: number): string;
3
+ export declare function mutateStages(lua: string, variantIndex: number): string;
4
+ export declare function mutateMocks(lua: string, variantIndex: number): string;
5
+ export declare function mutateScenarioVariants(source: ScenarioDescriptor, count: number): ScenarioDescriptor[];
@@ -0,0 +1,51 @@
1
+ function mutateNumericToken(token, delta) {
2
+ const asNumber = Number(token);
3
+ if (!Number.isFinite(asNumber)) {
4
+ return token;
5
+ }
6
+ return String(asNumber + delta);
7
+ }
8
+ export function mutateValues(lua, variantIndex) {
9
+ let replaced = false;
10
+ const next = lua.replace(/\b\d+(?:\.\d+)?\b/g, (token) => {
11
+ if (replaced) {
12
+ return token;
13
+ }
14
+ replaced = true;
15
+ return mutateNumericToken(token, variantIndex);
16
+ });
17
+ if (replaced) {
18
+ return next;
19
+ }
20
+ return `${lua}\n-- value mutation ${variantIndex}`;
21
+ }
22
+ export function mutateStages(lua, variantIndex) {
23
+ if (!lua.includes("stages")) {
24
+ return lua;
25
+ }
26
+ return `${lua}\n-- stage-order mutation ${variantIndex}`;
27
+ }
28
+ export function mutateMocks(lua, variantIndex) {
29
+ if (!lua.includes("given")) {
30
+ return lua;
31
+ }
32
+ return `${lua}\n-- mock-state mutation ${variantIndex}`;
33
+ }
34
+ export function mutateScenarioVariants(source, count) {
35
+ const variants = [];
36
+ for (let index = 1; index <= count; index += 1) {
37
+ const variantLua = source.lua
38
+ ? mutateMocks(mutateStages(mutateValues(source.lua, index), index), index)
39
+ : undefined;
40
+ const variant = {
41
+ ...source,
42
+ id: `${source.id}-mut-${index}`,
43
+ name: `${source.name} mutation ${index}`,
44
+ };
45
+ if (variantLua !== undefined) {
46
+ variant.lua = variantLua;
47
+ }
48
+ variants.push(variant);
49
+ }
50
+ return variants;
51
+ }
@@ -0,0 +1,10 @@
1
+ export { createMcpServer } from "./server.js";
2
+ export { createCoverageTools } from "./tools/coverage.js";
3
+ export { createComponentTools } from "./tools/component.js";
4
+ export { createMockTools } from "./tools/mock.js";
5
+ export { createScenarioTools } from "./tools/scenario.js";
6
+ export { generate } from "./generation/combinatorial.js";
7
+ export { mutateMocks, mutateScenarioVariants, mutateStages, mutateValues } from "./generation/mutator.js";
8
+ export { createResourceCatalog } from "./resources/index.js";
9
+ export { createPromptCatalog } from "./prompts/index.js";
10
+ export { parseJsonRpcLine, processJsonRpcLine, runStdioServer } from "./transport/stdio.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export { createMcpServer } from "./server.js";
2
+ export { createCoverageTools } from "./tools/coverage.js";
3
+ export { createComponentTools } from "./tools/component.js";
4
+ export { createMockTools } from "./tools/mock.js";
5
+ export { createScenarioTools } from "./tools/scenario.js";
6
+ export { generate } from "./generation/combinatorial.js";
7
+ export { mutateMocks, mutateScenarioVariants, mutateStages, mutateValues } from "./generation/mutator.js";
8
+ export { createResourceCatalog } from "./resources/index.js";
9
+ export { createPromptCatalog } from "./prompts/index.js";
10
+ export { parseJsonRpcLine, processJsonRpcLine, runStdioServer } from "./transport/stdio.js";
@@ -0,0 +1 @@
1
+ export declare function analyzeFailurePrompt(diff: string): string;
@@ -0,0 +1,3 @@
1
+ export function analyzeFailurePrompt(diff) {
2
+ return `Analyze this deterministic failure diff and identify root cause:\n${diff}`;
3
+ }
@@ -0,0 +1 @@
1
+ export declare function generateEdgeCasesPrompt(component: string): string;
@@ -0,0 +1,3 @@
1
+ export function generateEdgeCasesPrompt(component) {
2
+ return `Generate deterministic edge-case scenarios for ${component} using given/when/then_ui format.`;
3
+ }
@@ -0,0 +1 @@
1
+ export declare function improveCoveragePrompt(gaps: string[]): string;
@@ -0,0 +1,3 @@
1
+ export function improveCoveragePrompt(gaps) {
2
+ return `Suggest scenarios to cover these gaps: ${gaps.join(", ")}`;
3
+ }
@@ -0,0 +1,5 @@
1
+ export type PromptTemplate = {
2
+ id: string;
3
+ render: (input: string | string[]) => string;
4
+ };
5
+ export declare function createPromptCatalog(): PromptTemplate[];
@@ -0,0 +1,24 @@
1
+ import { analyzeFailurePrompt } from "./analyze-failure.js";
2
+ import { generateEdgeCasesPrompt } from "./generate-edge-cases.js";
3
+ import { improveCoveragePrompt } from "./improve-coverage.js";
4
+ import { regressionFromDiffPrompt } from "./regression-from-diff.js";
5
+ export function createPromptCatalog() {
6
+ return [
7
+ {
8
+ id: "generate-edge-cases",
9
+ render: (input) => generateEdgeCasesPrompt(String(input)),
10
+ },
11
+ {
12
+ id: "analyze-failure",
13
+ render: (input) => analyzeFailurePrompt(String(input)),
14
+ },
15
+ {
16
+ id: "improve-coverage",
17
+ render: (input) => improveCoveragePrompt(Array.isArray(input) ? input : [String(input)]),
18
+ },
19
+ {
20
+ id: "regression-from-diff",
21
+ render: (input) => regressionFromDiffPrompt(String(input)),
22
+ },
23
+ ];
24
+ }
@@ -0,0 +1 @@
1
+ export declare function regressionFromDiffPrompt(diffSummary: string): string;
@@ -0,0 +1,3 @@
1
+ export function regressionFromDiffPrompt(diffSummary) {
2
+ return `Generate regression scenarios from this code diff summary:\n${diffSummary}`;
3
+ }
@@ -0,0 +1,4 @@
1
+ export declare function componentsResource(tree: unknown): {
2
+ uri: string;
3
+ content: unknown;
4
+ };
@@ -0,0 +1,6 @@
1
+ export function componentsResource(tree) {
2
+ return {
3
+ uri: "lunatest://components",
4
+ content: tree,
5
+ };
6
+ }
@@ -0,0 +1,4 @@
1
+ export declare function coverageResource(report: Record<string, unknown>): {
2
+ uri: string;
3
+ content: Record<string, unknown>;
4
+ };
@@ -0,0 +1,6 @@
1
+ export function coverageResource(report) {
2
+ return {
3
+ uri: "lunatest://coverage",
4
+ content: report,
5
+ };
6
+ }
@@ -0,0 +1,7 @@
1
+ export declare function guideResource(): {
2
+ uri: string;
3
+ content: {
4
+ title: string;
5
+ sections: string[];
6
+ };
7
+ };
@@ -0,0 +1,13 @@
1
+ export function guideResource() {
2
+ return {
3
+ uri: "lunatest://guide",
4
+ content: {
5
+ title: "LunaTest Scenario Guide",
6
+ sections: [
7
+ "Define given state first",
8
+ "Describe action in when.action",
9
+ "Assert ui/state deterministically",
10
+ ],
11
+ },
12
+ };
13
+ }
@@ -0,0 +1,11 @@
1
+ import type { ScenarioDescriptor } from "../tools/scenario.js";
2
+ export type McpResource = {
3
+ uri: string;
4
+ content: unknown;
5
+ };
6
+ export declare function createResourceCatalog(options: {
7
+ scenarios: ScenarioDescriptor[];
8
+ coverage: Record<string, unknown>;
9
+ components: unknown;
10
+ protocols: string[];
11
+ }): McpResource[];
@@ -0,0 +1,16 @@
1
+ import { componentsResource } from "./components.js";
2
+ import { coverageResource } from "./coverage.js";
3
+ import { guideResource } from "./guide.js";
4
+ import { mocksResource } from "./mocks.js";
5
+ import { protocolsResource } from "./protocols.js";
6
+ import { scenariosResource } from "./scenarios.js";
7
+ export function createResourceCatalog(options) {
8
+ return [
9
+ scenariosResource(options.scenarios),
10
+ coverageResource(options.coverage),
11
+ componentsResource(options.components),
12
+ mocksResource(),
13
+ protocolsResource(options.protocols),
14
+ guideResource(),
15
+ ];
16
+ }
@@ -0,0 +1,10 @@
1
+ export declare function mocksResource(): {
2
+ uri: string;
3
+ content: {
4
+ schema: {
5
+ chain: string[];
6
+ wallet: string[];
7
+ events: string[];
8
+ };
9
+ };
10
+ };
@@ -0,0 +1,12 @@
1
+ export function mocksResource() {
2
+ return {
3
+ uri: "lunatest://mocks",
4
+ content: {
5
+ schema: {
6
+ chain: ["id", "blockNumber"],
7
+ wallet: ["address", "balances", "allowances"],
8
+ events: ["atMs", "type", "payload"],
9
+ },
10
+ },
11
+ };
12
+ }
@@ -0,0 +1,4 @@
1
+ export declare function protocolsResource(protocols: string[]): {
2
+ uri: string;
3
+ content: string[];
4
+ };
@@ -0,0 +1,6 @@
1
+ export function protocolsResource(protocols) {
2
+ return {
3
+ uri: "lunatest://protocols",
4
+ content: protocols,
5
+ };
6
+ }
@@ -0,0 +1,5 @@
1
+ import type { ScenarioDescriptor } from "../tools/scenario.js";
2
+ export declare function scenariosResource(scenarios: ScenarioDescriptor[]): {
3
+ uri: string;
4
+ content: ScenarioDescriptor[];
5
+ };
@@ -0,0 +1,6 @@
1
+ export function scenariosResource(scenarios) {
2
+ return {
3
+ uri: "lunatest://scenarios",
4
+ content: scenarios,
5
+ };
6
+ }
@@ -0,0 +1,36 @@
1
+ import { type ScenarioDescriptor } from "./tools/scenario.js";
2
+ import type { ExecuteLuaScenarioInput } from "@lunatest/core";
3
+ type JsonRpcRequest = {
4
+ id: string;
5
+ method: string;
6
+ params?: Record<string, unknown>;
7
+ };
8
+ type JsonRpcResponse = {
9
+ id: string;
10
+ result?: unknown;
11
+ error?: {
12
+ message: string;
13
+ };
14
+ };
15
+ type McpServerOptions = {
16
+ scenarios?: ScenarioDescriptor[];
17
+ coverage?: {
18
+ total?: number;
19
+ covered?: number;
20
+ ratio?: number;
21
+ };
22
+ mockState?: Record<string, unknown>;
23
+ componentTree?: Array<{
24
+ name: string;
25
+ children?: Array<{
26
+ name: string;
27
+ }>;
28
+ }>;
29
+ componentStates?: Record<string, string[]>;
30
+ protocols?: string[];
31
+ scenarioAdapter?: ExecuteLuaScenarioInput["adapter"];
32
+ };
33
+ export declare function createMcpServer(options: McpServerOptions): {
34
+ handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse>;
35
+ };
36
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,205 @@
1
+ import { createPromptCatalog } from "./prompts/index.js";
2
+ import { createResourceCatalog } from "./resources/index.js";
3
+ import { createComponentTools } from "./tools/component.js";
4
+ import { createCoverageTools } from "./tools/coverage.js";
5
+ import { createMockTools } from "./tools/mock.js";
6
+ import { createScenarioTools, } from "./tools/scenario.js";
7
+ export function createMcpServer(options) {
8
+ const scenarioTools = createScenarioTools(options.scenarios ?? [], {
9
+ adapter: options.scenarioAdapter,
10
+ });
11
+ const coverageTools = createCoverageTools(options.coverage);
12
+ const mockTools = createMockTools(options.mockState);
13
+ const componentTools = createComponentTools(options.componentTree ?? [], options.componentStates ?? {});
14
+ const prompts = createPromptCatalog();
15
+ const getResources = async () => {
16
+ const scenarios = await scenarioTools.list();
17
+ const coverage = await coverageTools.report();
18
+ const components = await componentTools.tree();
19
+ return createResourceCatalog({
20
+ scenarios,
21
+ coverage,
22
+ components,
23
+ protocols: options.protocols ?? ["uniswap_v2", "uniswap_v3", "curve", "aave"],
24
+ });
25
+ };
26
+ return {
27
+ async handleRequest(request) {
28
+ try {
29
+ if (request.method === "scenario.list") {
30
+ return {
31
+ id: request.id,
32
+ result: await scenarioTools.list(),
33
+ };
34
+ }
35
+ if (request.method === "scenario.get") {
36
+ return {
37
+ id: request.id,
38
+ result: await scenarioTools.get(String(request.params?.id ?? "")),
39
+ };
40
+ }
41
+ if (request.method === "scenario.create") {
42
+ return {
43
+ id: request.id,
44
+ result: await scenarioTools.create({
45
+ id: request.params?.id === undefined
46
+ ? undefined
47
+ : String(request.params.id),
48
+ name: request.params?.name === undefined
49
+ ? undefined
50
+ : String(request.params.name),
51
+ lua: request.params?.lua === undefined
52
+ ? undefined
53
+ : String(request.params.lua),
54
+ }),
55
+ };
56
+ }
57
+ if (request.method === "scenario.run") {
58
+ return {
59
+ id: request.id,
60
+ result: await scenarioTools.run({
61
+ id: request.params?.id === undefined
62
+ ? undefined
63
+ : String(request.params.id),
64
+ lua: request.params?.lua === undefined
65
+ ? undefined
66
+ : String(request.params.lua),
67
+ }),
68
+ };
69
+ }
70
+ if (request.method === "scenario.runAll") {
71
+ return {
72
+ id: request.id,
73
+ result: await scenarioTools.runAll(request.params?.filter === undefined
74
+ ? undefined
75
+ : String(request.params.filter)),
76
+ };
77
+ }
78
+ if (request.method === "scenario.mutate") {
79
+ return {
80
+ id: request.id,
81
+ result: await scenarioTools.mutate({
82
+ id: String(request.params?.id ?? ""),
83
+ count: request.params?.count === undefined
84
+ ? undefined
85
+ : Number(request.params.count),
86
+ }),
87
+ };
88
+ }
89
+ if (request.method === "coverage.report") {
90
+ return {
91
+ id: request.id,
92
+ result: await coverageTools.report(),
93
+ };
94
+ }
95
+ if (request.method === "coverage.gaps") {
96
+ return {
97
+ id: request.id,
98
+ result: await coverageTools.gaps(),
99
+ };
100
+ }
101
+ if (request.method === "coverage.suggest") {
102
+ return {
103
+ id: request.id,
104
+ result: await coverageTools.suggest(),
105
+ };
106
+ }
107
+ if (request.method === "mock.getState") {
108
+ return {
109
+ id: request.id,
110
+ result: await mockTools.getState(),
111
+ };
112
+ }
113
+ if (request.method === "mock.setState") {
114
+ return {
115
+ id: request.id,
116
+ result: await mockTools.setState(request.params?.state ?? {}),
117
+ };
118
+ }
119
+ if (request.method === "state.patch") {
120
+ return {
121
+ id: request.id,
122
+ result: await mockTools.patchState(request.params?.state ?? {}),
123
+ };
124
+ }
125
+ if (request.method === "mock.routes.set") {
126
+ return {
127
+ id: request.id,
128
+ result: await mockTools.setRoutes(request.params?.routes ?? []),
129
+ };
130
+ }
131
+ if (request.method === "mock.routes.get") {
132
+ return {
133
+ id: request.id,
134
+ result: await mockTools.getRoutes(),
135
+ };
136
+ }
137
+ if (request.method === "mock.listPresets") {
138
+ return {
139
+ id: request.id,
140
+ result: await mockTools.listPresets(),
141
+ };
142
+ }
143
+ if (request.method === "component.tree") {
144
+ return {
145
+ id: request.id,
146
+ result: await componentTools.tree(),
147
+ };
148
+ }
149
+ if (request.method === "component.states") {
150
+ return {
151
+ id: request.id,
152
+ result: await componentTools.states(String(request.params?.name ?? "")),
153
+ };
154
+ }
155
+ if (request.method === "resource.list") {
156
+ const resources = await getResources();
157
+ return {
158
+ id: request.id,
159
+ result: resources.map((resource) => resource.uri),
160
+ };
161
+ }
162
+ if (request.method === "resource.get") {
163
+ const target = String(request.params?.uri ?? "");
164
+ const resources = await getResources();
165
+ return {
166
+ id: request.id,
167
+ result: resources.find((resource) => resource.uri === target) ?? null,
168
+ };
169
+ }
170
+ if (request.method === "prompt.list") {
171
+ return {
172
+ id: request.id,
173
+ result: prompts.map((prompt) => prompt.id),
174
+ };
175
+ }
176
+ if (request.method === "prompt.get") {
177
+ const target = String(request.params?.id ?? "");
178
+ const prompt = prompts.find((item) => item.id === target);
179
+ if (!prompt) {
180
+ throw new Error(`Prompt not found: ${target}`);
181
+ }
182
+ const input = request.params?.input;
183
+ const normalizedInput = input === undefined || input === null ? "" : input;
184
+ return {
185
+ id: request.id,
186
+ result: {
187
+ id: prompt.id,
188
+ text: prompt.render(normalizedInput),
189
+ },
190
+ };
191
+ }
192
+ throw new Error(`Unsupported method: ${request.method}`);
193
+ }
194
+ catch (error) {
195
+ const message = error instanceof Error ? error.message : String(error);
196
+ return {
197
+ id: request.id,
198
+ error: {
199
+ message,
200
+ },
201
+ };
202
+ }
203
+ },
204
+ };
205
+ }
@@ -0,0 +1,10 @@
1
+ type ComponentStateMap = Record<string, string[]>;
2
+ type ComponentNode = {
3
+ name: string;
4
+ children?: ComponentNode[];
5
+ };
6
+ export declare function createComponentTools(tree?: ComponentNode[], states?: ComponentStateMap): {
7
+ tree(): Promise<ComponentNode[]>;
8
+ states(name: string): Promise<string[]>;
9
+ };
10
+ export {};
@@ -0,0 +1,10 @@
1
+ export function createComponentTools(tree = [], states = {}) {
2
+ return {
3
+ async tree() {
4
+ return tree;
5
+ },
6
+ async states(name) {
7
+ return states[name] ?? [];
8
+ },
9
+ };
10
+ }
@@ -0,0 +1,16 @@
1
+ export type CoverageReport = {
2
+ total: number;
3
+ covered: number;
4
+ ratio: number;
5
+ };
6
+ export declare function createCoverageTools(seed?: Partial<CoverageReport>): {
7
+ report(): Promise<CoverageReport>;
8
+ gaps(): Promise<{
9
+ id: string;
10
+ reason: string;
11
+ }[]>;
12
+ suggest(): Promise<{
13
+ id: string;
14
+ title: string;
15
+ }[]>;
16
+ };
@@ -0,0 +1,29 @@
1
+ export function createCoverageTools(seed) {
2
+ const report = {
3
+ total: seed?.total ?? 0,
4
+ covered: seed?.covered ?? 0,
5
+ ratio: seed?.ratio ??
6
+ ((seed?.total ?? 0) === 0
7
+ ? 1
8
+ : Number(((seed?.covered ?? 0) / (seed?.total ?? 0)).toFixed(4))),
9
+ };
10
+ return {
11
+ async report() {
12
+ return report;
13
+ },
14
+ async gaps() {
15
+ const missing = Math.max(0, report.total - report.covered);
16
+ return Array.from({ length: missing }, (_, index) => ({
17
+ id: `gap-${index + 1}`,
18
+ reason: "scenario not covered",
19
+ }));
20
+ },
21
+ async suggest() {
22
+ const missing = Math.max(0, report.total - report.covered);
23
+ return Array.from({ length: missing }, (_, index) => ({
24
+ id: `suggestion-${index + 1}`,
25
+ title: `Add edge case scenario ${index + 1}`,
26
+ }));
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,8 @@
1
+ export declare function createMockTools(initialState?: Record<string, unknown>): {
2
+ getState(): Promise<Record<string, unknown>>;
3
+ setState(next: Record<string, unknown>): Promise<Record<string, unknown>>;
4
+ patchState(partial: Record<string, unknown>): Promise<Record<string, unknown>>;
5
+ setRoutes(nextRoutes: Array<Record<string, unknown>>): Promise<Record<string, unknown>[]>;
6
+ getRoutes(): Promise<Record<string, unknown>[]>;
7
+ listPresets(): Promise<string[]>;
8
+ };
@@ -0,0 +1,29 @@
1
+ import { deepClone, deepMerge } from "@lunatest/contracts";
2
+ export function createMockTools(initialState = {}) {
3
+ let state = deepClone(initialState);
4
+ let routes = [];
5
+ const presets = ["uniswap_v2", "uniswap_v3", "curve", "aave"];
6
+ return {
7
+ async getState() {
8
+ return deepClone(state);
9
+ },
10
+ async setState(next) {
11
+ state = deepClone(next);
12
+ return deepClone(state);
13
+ },
14
+ async patchState(partial) {
15
+ state = deepMerge(state, partial);
16
+ return deepClone(state);
17
+ },
18
+ async setRoutes(nextRoutes) {
19
+ routes = deepClone(nextRoutes);
20
+ return deepClone(routes);
21
+ },
22
+ async getRoutes() {
23
+ return deepClone(routes);
24
+ },
25
+ async listPresets() {
26
+ return [...presets];
27
+ },
28
+ };
29
+ }
@@ -0,0 +1,44 @@
1
+ import { type ExecuteLuaScenarioInput } from "@lunatest/core";
2
+ export type ScenarioDescriptor = {
3
+ id: string;
4
+ name: string;
5
+ lua?: string;
6
+ tags?: string[];
7
+ };
8
+ type ScenarioCreateInput = {
9
+ id?: string;
10
+ name?: string;
11
+ lua?: string;
12
+ tags?: string[];
13
+ };
14
+ type ScenarioMutateInput = {
15
+ id: string;
16
+ count?: number;
17
+ };
18
+ type ScenarioRunInput = string | {
19
+ id?: string;
20
+ lua?: string;
21
+ };
22
+ export type ScenarioTools = {
23
+ list: () => Promise<ScenarioDescriptor[]>;
24
+ get: (id: string) => Promise<ScenarioDescriptor | null>;
25
+ create: (input: ScenarioCreateInput) => Promise<ScenarioDescriptor>;
26
+ run: (input: ScenarioRunInput) => Promise<{
27
+ id: string;
28
+ pass: boolean;
29
+ error?: string;
30
+ diff?: string;
31
+ }>;
32
+ runAll: (filter?: string) => Promise<Array<{
33
+ id: string;
34
+ pass: boolean;
35
+ error?: string;
36
+ diff?: string;
37
+ }>>;
38
+ mutate: (input: ScenarioMutateInput) => Promise<ScenarioDescriptor[]>;
39
+ };
40
+ type CreateScenarioToolsOptions = {
41
+ adapter?: ExecuteLuaScenarioInput["adapter"];
42
+ };
43
+ export declare function createScenarioTools(seed: ScenarioDescriptor[], options?: CreateScenarioToolsOptions): ScenarioTools;
44
+ export {};
@@ -0,0 +1,96 @@
1
+ import { mutateScenarioVariants } from "../generation/mutator.js";
2
+ import { executeLuaScenario } from "@lunatest/core";
3
+ async function runLua(source, adapter) {
4
+ const execution = await executeLuaScenario({
5
+ source,
6
+ adapter,
7
+ });
8
+ return {
9
+ pass: execution.pass,
10
+ error: execution.error,
11
+ diff: execution.result?.diff,
12
+ };
13
+ }
14
+ export function createScenarioTools(seed, options = {}) {
15
+ const store = new Map(seed.map((item) => [item.id, { ...item }]));
16
+ return {
17
+ async list() {
18
+ return Array.from(store.values());
19
+ },
20
+ async get(id) {
21
+ return store.get(id) ?? null;
22
+ },
23
+ async create(input) {
24
+ const nextId = input.id && input.id.length > 0 ? input.id : `scenario-${store.size + 1}`;
25
+ const nextName = input.name && input.name.length > 0 ? input.name : `scenario ${store.size + 1}`;
26
+ const next = {
27
+ id: nextId,
28
+ name: nextName,
29
+ lua: input.lua,
30
+ tags: input.tags,
31
+ };
32
+ store.set(next.id, next);
33
+ return next;
34
+ },
35
+ async run(input) {
36
+ if (typeof input !== "string" && input.lua) {
37
+ const executed = await runLua(input.lua, options.adapter);
38
+ return {
39
+ id: "inline",
40
+ ...executed,
41
+ };
42
+ }
43
+ const id = typeof input === "string" ? input : String(input.id ?? "");
44
+ const scenario = store.get(id);
45
+ if (!scenario) {
46
+ throw new Error(`Scenario not found: ${id}`);
47
+ }
48
+ if (!scenario.lua) {
49
+ return {
50
+ id,
51
+ pass: false,
52
+ error: "scenario_lua_missing",
53
+ };
54
+ }
55
+ const executed = await runLua(scenario.lua, options.adapter);
56
+ return {
57
+ id,
58
+ ...executed,
59
+ };
60
+ },
61
+ async runAll(filter) {
62
+ const candidates = Array.from(store.values()).filter((scenario) => {
63
+ if (!filter) {
64
+ return true;
65
+ }
66
+ return scenario.id.includes(filter) || scenario.name.includes(filter);
67
+ });
68
+ return Promise.all(candidates.map(async (item) => {
69
+ if (!item.lua) {
70
+ return {
71
+ id: item.id,
72
+ pass: false,
73
+ error: "scenario_lua_missing",
74
+ };
75
+ }
76
+ const executed = await runLua(item.lua, options.adapter);
77
+ return {
78
+ id: item.id,
79
+ ...executed,
80
+ };
81
+ }));
82
+ },
83
+ async mutate(input) {
84
+ const source = store.get(input.id);
85
+ if (!source) {
86
+ throw new Error(`Scenario not found: ${input.id}`);
87
+ }
88
+ const count = Math.max(1, Math.trunc(input.count ?? 1));
89
+ const variants = mutateScenarioVariants(source, count);
90
+ for (const variant of variants) {
91
+ store.set(variant.id, variant);
92
+ }
93
+ return variants;
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,24 @@
1
+ import type { createMcpServer } from "../server.js";
2
+ type McpServer = ReturnType<typeof createMcpServer>;
3
+ type JsonRpcLikeRequest = {
4
+ id: string;
5
+ method: string;
6
+ params?: Record<string, unknown>;
7
+ };
8
+ type JsonRpcLikeResponse = {
9
+ id: string;
10
+ result?: unknown;
11
+ error?: {
12
+ message: string;
13
+ };
14
+ };
15
+ export type StdioServerOptions = {
16
+ input: NodeJS.ReadableStream;
17
+ output: NodeJS.WritableStream;
18
+ error?: NodeJS.WritableStream;
19
+ server: McpServer;
20
+ };
21
+ export declare function parseJsonRpcLine(rawLine: string): JsonRpcLikeRequest | JsonRpcLikeResponse;
22
+ export declare function processJsonRpcLine(line: string, server: McpServer): Promise<JsonRpcLikeResponse | null>;
23
+ export declare function runStdioServer(options: StdioServerOptions): Promise<void>;
24
+ export {};
@@ -0,0 +1,63 @@
1
+ import { createInterface } from "node:readline";
2
+ import { isRecord } from "@lunatest/contracts";
3
+ function invalidRequestResponse(message) {
4
+ return {
5
+ id: "unknown",
6
+ error: {
7
+ message,
8
+ },
9
+ };
10
+ }
11
+ function isJsonRpcLikeRequest(value) {
12
+ return "method" in value;
13
+ }
14
+ export function parseJsonRpcLine(rawLine) {
15
+ const trimmed = rawLine.trim();
16
+ if (!trimmed) {
17
+ return invalidRequestResponse("Empty JSON-RPC line");
18
+ }
19
+ let parsed;
20
+ try {
21
+ parsed = JSON.parse(trimmed);
22
+ }
23
+ catch {
24
+ return invalidRequestResponse("Malformed JSON line");
25
+ }
26
+ if (!isRecord(parsed)) {
27
+ return invalidRequestResponse("Invalid JSON-RPC payload");
28
+ }
29
+ const id = parsed.id;
30
+ const method = parsed.method;
31
+ if (typeof id !== "string" || typeof method !== "string") {
32
+ return invalidRequestResponse("JSON-RPC request requires string id and method");
33
+ }
34
+ const params = parsed.params !== undefined && isRecord(parsed.params)
35
+ ? parsed.params
36
+ : undefined;
37
+ return {
38
+ id,
39
+ method,
40
+ params,
41
+ };
42
+ }
43
+ export async function processJsonRpcLine(line, server) {
44
+ const parsed = parseJsonRpcLine(line);
45
+ if (!isJsonRpcLikeRequest(parsed)) {
46
+ return parsed;
47
+ }
48
+ return server.handleRequest(parsed);
49
+ }
50
+ export async function runStdioServer(options) {
51
+ const rl = createInterface({
52
+ input: options.input,
53
+ crlfDelay: Infinity,
54
+ });
55
+ for await (const line of rl) {
56
+ const response = await processJsonRpcLine(line, options.server);
57
+ if (!response) {
58
+ continue;
59
+ }
60
+ options.output.write(`${JSON.stringify(response)}\n`);
61
+ }
62
+ rl.close();
63
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@lunatest/mcp",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "lunatest-mcp": "./dist/bin/mcp-stdio.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public",
21
+ "tag": "latest"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "dependencies": {
27
+ "@lunatest/core": "0.1.0",
28
+ "@lunatest/contracts": "0.1.0"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc -p tsconfig.json",
32
+ "test": "vitest run",
33
+ "lint": "tsc -p tsconfig.json --noEmit"
34
+ }
35
+ }