@acid-cat/cli 0.1.0 → 0.3.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.
@@ -0,0 +1,98 @@
1
+ import { Checks, expect } from "./check.js";
2
+ import { mcpClient } from "./mcp.js";
3
+ import fc from "fast-check";
4
+ //#region src/conform/properties.ts
5
+ const RULE = "spec/invocation.md#cancellation-and-concurrency";
6
+ function httpInvoke(base) {
7
+ return async (id, input, signal) => {
8
+ return await (await fetch(`${base}/capabilities/${id}`, {
9
+ method: "POST",
10
+ headers: { "Content-Type": "application/json" },
11
+ body: JSON.stringify(input),
12
+ ...signal ? { signal } : {}
13
+ })).json();
14
+ };
15
+ }
16
+ async function mcpInvoke(url) {
17
+ const client = await mcpClient(url);
18
+ return {
19
+ invoke: async (id, input, signal) => {
20
+ try {
21
+ const result = await client.callTool({
22
+ name: id,
23
+ arguments: input
24
+ }, signal ? { signal } : void 0);
25
+ const body = JSON.parse(result.content[0]?.text ?? "null");
26
+ if (result.isError !== true) return {
27
+ ok: true,
28
+ value: result.structuredContent ?? body
29
+ };
30
+ return body;
31
+ } catch (error) {
32
+ if (error.code === -32602) return {
33
+ kind: "unknown-capability",
34
+ message: String(error)
35
+ };
36
+ throw error;
37
+ }
38
+ },
39
+ close: () => client.close()
40
+ };
41
+ }
42
+ const operation = fc.oneof(fc.string({ maxLength: 40 }).map((text) => ({
43
+ kind: "echo",
44
+ text
45
+ })), fc.constant({ kind: "refuse" }), fc.constant({ kind: "crash" }), fc.constant({ kind: "invalid" }));
46
+ /** The outcome that an operation must have. */
47
+ function agrees(operation, outcome) {
48
+ switch (operation.kind) {
49
+ case "echo": return "ok" in outcome && outcome.ok && JSON.stringify(outcome.value) === JSON.stringify({ text: operation.text });
50
+ case "refuse": return "ok" in outcome && !outcome.ok && outcome.error._tag === "Refused";
51
+ case "crash": return "kind" in outcome && outcome.kind === "internal";
52
+ case "invalid": return "kind" in outcome && outcome.kind === "invalid-input";
53
+ }
54
+ }
55
+ const callFor = (invoke, operation) => {
56
+ switch (operation.kind) {
57
+ case "echo": return invoke("fixture.echo", { text: operation.text });
58
+ case "refuse": return invoke("fixture.refuse", {});
59
+ case "crash": return invoke("fixture.crash", {});
60
+ case "invalid": return invoke("fixture.echo", { text: 0 });
61
+ }
62
+ };
63
+ async function checkProperties(binding, invoke) {
64
+ const checks = new Checks(`${binding} properties`);
65
+ await checks.run("concurrent calls each get exactly their own outcome", RULE, async () => {
66
+ await fc.assert(fc.asyncProperty(fc.array(operation, {
67
+ minLength: 1,
68
+ maxLength: 12
69
+ }), async (operations) => {
70
+ const outcomes = await Promise.all(operations.map((op) => callFor(invoke, op)));
71
+ operations.forEach((op, index) => expect(agrees(op, outcomes[index]), `${op.kind} gave ${JSON.stringify(outcomes[index])}`));
72
+ }), { numRuns: 25 });
73
+ });
74
+ await checks.run("cancelling a call affects only that call", RULE, async () => {
75
+ await fc.assert(fc.asyncProperty(fc.array(fc.boolean(), {
76
+ minLength: 2,
77
+ maxLength: 5
78
+ }), async (cancel) => {
79
+ const calls = cancel.map((cancelled) => {
80
+ const controller = new AbortController();
81
+ const outcome = invoke("fixture.slow", { milliseconds: 200 }, controller.signal).catch(() => "aborted");
82
+ if (cancelled) setTimeout(() => controller.abort(), 20);
83
+ return {
84
+ cancelled,
85
+ outcome
86
+ };
87
+ });
88
+ for (const call of calls) {
89
+ const outcome = await call.outcome;
90
+ if (!call.cancelled) expect(outcome !== "aborted" && "ok" in outcome && outcome.ok, `a call that was not cancelled gave ${JSON.stringify(outcome)}`);
91
+ else expect(outcome === "aborted" || "kind" in outcome && outcome.kind === "cancelled", `a cancelled call gave ${JSON.stringify(outcome)}`);
92
+ }
93
+ }), { numRuns: 5 });
94
+ });
95
+ return checks;
96
+ }
97
+ //#endregion
98
+ export { checkProperties, httpInvoke, mcpInvoke };
@@ -0,0 +1,62 @@
1
+ import { createServer } from "node:http";
2
+ //#region src/conform/serve.ts
3
+ /**
4
+ * Serve a web request handler, `(request: Request) => Promise<Response>`, on a local port. Implementations use it
5
+ * to run `acid-cat conform` against their bindings in a test.
6
+ */
7
+ /** Serve handlers by path prefix, for example `{ "/mcp": mcp, "/api": http }`. */
8
+ function serve(routes) {
9
+ const prefixes = Object.keys(routes).sort((a, b) => b.length - a.length);
10
+ const server = createServer((incoming, outgoing) => {
11
+ answer(incoming, outgoing, prefixes, routes);
12
+ });
13
+ return new Promise((resolve) => {
14
+ server.listen(0, "127.0.0.1", () => {
15
+ const address = server.address();
16
+ resolve({
17
+ url: `http://127.0.0.1:${typeof address === "object" && address !== null ? address.port : 0}`,
18
+ close: () => new Promise((done) => server.close(() => done()))
19
+ });
20
+ });
21
+ });
22
+ }
23
+ async function answer(incoming, outgoing, prefixes, routes) {
24
+ const url = new URL(incoming.url ?? "/", `http://${incoming.headers.host ?? "127.0.0.1"}`);
25
+ const prefix = prefixes.find((candidate) => url.pathname === candidate || url.pathname.startsWith(`${candidate}/`));
26
+ if (prefix === void 0) {
27
+ outgoing.writeHead(404).end();
28
+ return;
29
+ }
30
+ const abort = new AbortController();
31
+ outgoing.on("close", () => {
32
+ if (!outgoing.writableFinished) abort.abort();
33
+ });
34
+ const headers = new Headers();
35
+ for (const [name, value] of Object.entries(incoming.headers)) if (Array.isArray(value)) value.forEach((item) => headers.append(name, item));
36
+ else if (value !== void 0) headers.set(name, value);
37
+ const hasBody = incoming.method !== "GET" && incoming.method !== "HEAD";
38
+ const request = new Request(url, {
39
+ method: incoming.method ?? "GET",
40
+ headers,
41
+ signal: abort.signal,
42
+ ...hasBody ? {
43
+ body: incoming,
44
+ duplex: "half"
45
+ } : {}
46
+ });
47
+ try {
48
+ const response = await routes[prefix](request);
49
+ outgoing.writeHead(response.status, Object.fromEntries(response.headers));
50
+ if (response.body === null) {
51
+ outgoing.end();
52
+ return;
53
+ }
54
+ for await (const chunk of response.body) outgoing.write(chunk);
55
+ outgoing.end();
56
+ } catch {
57
+ if (!outgoing.headersSent) outgoing.writeHead(500);
58
+ outgoing.end();
59
+ }
60
+ }
61
+ //#endregion
62
+ export { serve };
@@ -0,0 +1,179 @@
1
+ import { Checks, expect, expectEqual, fixture, idsOf, sorted } from "./check.js";
2
+ import { REFERENCE, visitObjects } from "@acid-cat/spec-ts";
3
+ //#region src/conform/webmcp.ts
4
+ /**
5
+ * Conformance checks for the WebMCP binding (spec/bindings/webmcp.md).
6
+ *
7
+ * The checks act as the WebMCP host: they give the page a `modelContext`, record the tools that the binding registers,
8
+ * and call them. The page can run in a browser, or the implementation can install itself on the host in the same
9
+ * process.
10
+ */
11
+ const RULE = "spec/bindings/webmcp.md";
12
+ /** The target of the fixture that has the WebMCP binding. */
13
+ const TARGET = "app";
14
+ /**
15
+ * Make a test host. The function uses no imports and no outer variables, because the browser driver sends its
16
+ * source text to the page.
17
+ */
18
+ function createTestHost() {
19
+ const registered = /* @__PURE__ */ new Map();
20
+ return {
21
+ modelContext: { registerTool(tool, options) {
22
+ const recorded = tool;
23
+ registered.set(recorded.name, recorded);
24
+ options.signal.addEventListener("abort", () => {
25
+ if (registered.get(recorded.name) === recorded) registered.delete(recorded.name);
26
+ });
27
+ } },
28
+ tools: () => [...registered.values()].map((tool) => ({
29
+ name: tool.name,
30
+ description: tool.description,
31
+ inputSchema: tool.inputSchema
32
+ })),
33
+ async call(name, input, cancelAfter) {
34
+ const tool = registered.get(name);
35
+ if (tool === void 0) return { missing: true };
36
+ const controller = new AbortController();
37
+ if (cancelAfter !== void 0) setTimeout(() => controller.abort(), cancelAfter);
38
+ try {
39
+ return { resolved: await tool.execute(input, { signal: controller.signal }) };
40
+ } catch (error) {
41
+ const { name: errorName, kind, message } = error;
42
+ return { rejected: {
43
+ name: String(errorName),
44
+ kind,
45
+ message: String(message)
46
+ } };
47
+ }
48
+ }
49
+ };
50
+ }
51
+ /** A driver for an implementation in the same process. Values pass through JSON, as they do from a page. */
52
+ async function processDriver(install) {
53
+ const host = createTestHost();
54
+ const remove = await install(host.modelContext);
55
+ const copy = (value) => JSON.parse(JSON.stringify(value));
56
+ return {
57
+ tools: async () => copy(host.tools()),
58
+ call: async (name, input, cancelAfter) => copy(await host.call(name, input, cancelAfter)),
59
+ close: async () => {
60
+ if (typeof remove === "function") await remove();
61
+ }
62
+ };
63
+ }
64
+ /** A driver for a page in Chromium, through Playwright. The page loads with the test host as its `modelContext`. */
65
+ async function browserDriver(url) {
66
+ let playwright;
67
+ try {
68
+ playwright = await import("playwright");
69
+ } catch {
70
+ throw new Error("acid-cat conform --webmcp needs Playwright and Chromium: `pnpm add -D playwright`, then `pnpm exec playwright install chromium`.");
71
+ }
72
+ const browser = await playwright.chromium.launch();
73
+ const page = await browser.newPage();
74
+ const errors = [];
75
+ page.on("pageerror", (error) => errors.push(error.message));
76
+ await page.addInitScript(`
77
+ window.__acidCatConform = (${createTestHost.toString()})();
78
+ for (const scope of [document, navigator]) {
79
+ Object.defineProperty(scope, "modelContext", { value: window.__acidCatConform.modelContext, configurable: true });
80
+ }
81
+ `);
82
+ await page.goto(url);
83
+ return {
84
+ tools: async () => {
85
+ if (errors.length > 0) throw new Error(`The page has errors: ${errors.join("; ")}`);
86
+ return page.evaluate(() => window.__acidCatConform.tools());
87
+ },
88
+ call: (name, input, cancelAfter) => page.evaluate(([name, input, cancelAfter]) => window.__acidCatConform.call(name, input, cancelAfter), [
89
+ name,
90
+ input,
91
+ cancelAfter
92
+ ]),
93
+ close: () => browser.close()
94
+ };
95
+ }
96
+ /** Wait until `condition` is true. Give up after `timeout` milliseconds. */
97
+ async function eventually(condition, what, timeout = 5e3) {
98
+ const end = Date.now() + timeout;
99
+ for (;;) {
100
+ if (await condition()) return;
101
+ if (Date.now() > end) throw new Error(`after ${timeout} ms: ${what}`);
102
+ await new Promise((resolve) => setTimeout(resolve, 20));
103
+ }
104
+ }
105
+ /** True if each `$ref` of a schema refers to a type in its own `$defs` (spec/catalog.md#bundling). */
106
+ function isBundled(schema) {
107
+ const types = schema.$defs ?? {};
108
+ let bundled = true;
109
+ visitObjects(schema, (node) => {
110
+ if (typeof node.$ref !== "string") return;
111
+ const name = REFERENCE.exec(node.$ref)?.[1];
112
+ if (name === void 0 || !(name in types)) bundled = false;
113
+ });
114
+ return bundled;
115
+ }
116
+ const names = async (driver) => sorted((await driver.tools()).map((tool) => tool.name));
117
+ async function checkWebMcp(driver) {
118
+ const checks = new Checks("webmcp");
119
+ const view = fixture.capabilities;
120
+ const expected = idsOf(TARGET);
121
+ await checks.run("each available capability of the view is a tool", `${RULE}#tools`, async () => {
122
+ await eventually(async () => JSON.stringify(await names(driver)) === JSON.stringify(expected), `the tools must be ${expected.join(", ")}`);
123
+ });
124
+ await checks.run("a tool has the description and the bundled input schema of its capability", `${RULE}#tools`, async () => {
125
+ for (const tool of await driver.tools()) {
126
+ expectEqual(tool.description, view[tool.name]?.description, `description of ${tool.name}`);
127
+ const schema = tool.inputSchema;
128
+ const input = view[tool.name]?.input ?? {};
129
+ expectEqual(schema.type, "object", `type of the input schema of ${tool.name}`);
130
+ expectEqual(sorted(Object.keys(schema.properties ?? {})), sorted(Object.keys(input.properties ?? {})), `properties of ${tool.name}`);
131
+ expectEqual(sorted(schema.required ?? []), sorted(input.required ?? []), `required properties of ${tool.name}`);
132
+ expect(isBundled(schema), `the input schema of ${tool.name} must be bundled`);
133
+ }
134
+ });
135
+ await checks.run("a value resolves as a result", `${RULE}#tools`, async () => {
136
+ expectEqual(await driver.call("fixture.echo", { text: "hello" }), { resolved: {
137
+ ok: true,
138
+ value: { text: "hello" }
139
+ } }, "outcome");
140
+ });
141
+ await checks.run("a failure resolves as a result", `${RULE}#tools`, async () => {
142
+ const call = await driver.call("fixture.refuse", {});
143
+ expect("resolved" in call, `the call must resolve, got ${JSON.stringify(call)}`);
144
+ expectEqual(call.resolved.error?._tag, "Refused", "_tag");
145
+ });
146
+ for (const [name, capability, input, kind, errorName] of [[
147
+ "a defect rejects with the kind internal",
148
+ "fixture.crash",
149
+ {},
150
+ "internal",
151
+ void 0
152
+ ], [
153
+ "invalid input rejects with the kind invalid-input, as a TypeError",
154
+ "fixture.echo",
155
+ { text: 1 },
156
+ "invalid-input",
157
+ "TypeError"
158
+ ]]) await checks.run(name, `${RULE}#tools`, async () => {
159
+ const call = await driver.call(capability, input);
160
+ expect("rejected" in call, `the call must reject, got ${JSON.stringify(call)}`);
161
+ expectEqual(call.rejected.kind, kind, "kind");
162
+ if (errorName !== void 0) expectEqual(call.rejected.name, errorName, "name");
163
+ });
164
+ await checks.run("the signal of the host cancels a call, as an AbortError", `${RULE}#tools`, async () => {
165
+ const call = await driver.call("fixture.slow", { milliseconds: 6e4 }, 50);
166
+ expect("rejected" in call, `the call must reject, got ${JSON.stringify(call)}`);
167
+ expectEqual(call.rejected.kind, "cancelled", "kind");
168
+ expectEqual(call.rejected.name, "AbortError", "name");
169
+ });
170
+ await checks.run("a tool is removed while its capability is unavailable, and registered again after", `${RULE}#tools`, async () => {
171
+ await driver.call("fixture.switch.set", { on: false });
172
+ await eventually(async () => !(await names(driver)).includes("fixture.toggle"), "fixture.toggle must be removed");
173
+ await driver.call("fixture.switch.set", { on: true });
174
+ await eventually(async () => (await names(driver)).includes("fixture.toggle"), "fixture.toggle must be registered again");
175
+ });
176
+ return checks;
177
+ }
178
+ //#endregion
179
+ export { browserDriver, checkWebMcp, createTestHost, processDriver };
package/dist/index.js CHANGED
@@ -1,16 +1,5 @@
1
- import {
2
- bundle,
3
- check,
4
- conform,
5
- fixture,
6
- serve,
7
- viewOf
8
- } from "./chunks/chunk-VZ6KDZEL.js";
9
- export {
10
- bundle,
11
- check,
12
- conform,
13
- fixture,
14
- serve,
15
- viewOf
16
- };
1
+ import { bundle, check, viewOf } from "./catalog.js";
2
+ import { fixture } from "./conform/check.js";
3
+ import { serve } from "./conform/serve.js";
4
+ import { conform } from "./conform/index.js";
5
+ export { bundle, check, conform, fixture, serve, viewOf };
@@ -0,0 +1,117 @@
1
+ //#region ../../schemas/catalog.schema.json
2
+ var catalog_schema_default = {
3
+ $schema: "https://json-schema.org/draft/2020-12/schema",
4
+ $id: "https://acid-cat.dev/schemas/0.1/catalog.schema.json",
5
+ title: "AcidCat catalog",
6
+ description: "One document that states what an application can do and which targets see it. See spec/catalog.md.",
7
+ type: "object",
8
+ required: [
9
+ "acidcat",
10
+ "targets",
11
+ "capabilities"
12
+ ],
13
+ properties: {
14
+ "acidcat": {
15
+ "description": "The version of the AcidCat specification this catalog follows.",
16
+ "const": "0.1"
17
+ },
18
+ "title": { "type": "string" },
19
+ "description": { "type": "string" },
20
+ "targets": {
21
+ "description": "The targets, keyed by name.",
22
+ "type": "object",
23
+ "propertyNames": { "$ref": "#/$defs/targetName" },
24
+ "additionalProperties": { "$ref": "#/$defs/target" }
25
+ },
26
+ "$defs": {
27
+ "description": "Shared types, keyed by name. Capability schemas reference them as #/$defs/<Name>.",
28
+ "type": "object",
29
+ "propertyNames": { "$ref": "#/$defs/typeName" },
30
+ "additionalProperties": { "$ref": "#/$defs/schema" }
31
+ },
32
+ "capabilities": {
33
+ "description": "The capabilities, keyed by id.",
34
+ "type": "object",
35
+ "propertyNames": { "$ref": "#/$defs/id" },
36
+ "additionalProperties": { "$ref": "#/$defs/capability" }
37
+ }
38
+ },
39
+ $defs: {
40
+ "id": {
41
+ "type": "string",
42
+ "maxLength": 128,
43
+ "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*(\\.[a-z][a-z0-9]*(-[a-z0-9]+)*)+$"
44
+ },
45
+ "targetName": {
46
+ "type": "string",
47
+ "pattern": "^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$"
48
+ },
49
+ "bindingName": {
50
+ "type": "string",
51
+ "pattern": "^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$"
52
+ },
53
+ "typeName": {
54
+ "type": "string",
55
+ "pattern": "^[A-Za-z][A-Za-z0-9_]*$"
56
+ },
57
+ "target": {
58
+ "type": "object",
59
+ "required": ["bindings"],
60
+ "properties": {
61
+ "bindings": {
62
+ "type": "array",
63
+ "items": { "$ref": "#/$defs/bindingName" },
64
+ "minItems": 1,
65
+ "uniqueItems": true
66
+ },
67
+ "description": { "type": "string" },
68
+ "options": { "type": "object" }
69
+ }
70
+ },
71
+ "schema": {
72
+ "description": "A JSON Schema 2020-12 schema. Its $refs point into the catalog's $defs.",
73
+ "type": ["object", "boolean"]
74
+ },
75
+ "objectSchema": {
76
+ "description": "An input schema: it states that the input is an object, or it references a type.",
77
+ "type": "object",
78
+ "anyOf": [{
79
+ "required": ["type"],
80
+ "properties": { "type": { "const": "object" } }
81
+ }, {
82
+ "required": ["$ref"],
83
+ "properties": { "$ref": { "type": "string" } }
84
+ }]
85
+ },
86
+ "capability": {
87
+ "type": "object",
88
+ "required": ["description"],
89
+ "properties": {
90
+ "description": {
91
+ "type": "string",
92
+ "minLength": 1,
93
+ "pattern": "\\S"
94
+ },
95
+ "audience": {
96
+ "type": "array",
97
+ "items": { "$ref": "#/$defs/targetName" },
98
+ "uniqueItems": true
99
+ },
100
+ "input": { "$ref": "#/$defs/objectSchema" },
101
+ "output": { "$ref": "#/$defs/schema" },
102
+ "failures": {
103
+ "type": "object",
104
+ "propertyNames": {
105
+ "type": "string",
106
+ "minLength": 1
107
+ },
108
+ "additionalProperties": { "$ref": "#/$defs/schema" }
109
+ },
110
+ "readOnly": { "type": "boolean" },
111
+ "state": { "$ref": "#/$defs/schema" }
112
+ }
113
+ }
114
+ }
115
+ };
116
+ //#endregion
117
+ export { catalog_schema_default as default };
@@ -0,0 +1,140 @@
1
+ //#region conformance/fixture.acidcat.json
2
+ var fixture_acidcat_default = {
3
+ acidcat: "0.1",
4
+ title: "AcidCat conformance fixture",
5
+ description: "One capability for each behavior that acid-cat conform tests.",
6
+ targets: {
7
+ "api": { "bindings": ["http"] },
8
+ "app": { "bindings": ["typescript", "webmcp"] },
9
+ "cli": { "bindings": ["cli"] },
10
+ "mcp": { "bindings": ["mcp"] }
11
+ },
12
+ capabilities: {
13
+ "fixture.crash": {
14
+ "description": "Always fail with a defect.",
15
+ "audience": [
16
+ "app",
17
+ "mcp",
18
+ "cli",
19
+ "api"
20
+ ]
21
+ },
22
+ "fixture.echo": {
23
+ "description": "Return the input text.",
24
+ "audience": [
25
+ "app",
26
+ "mcp",
27
+ "cli",
28
+ "api"
29
+ ],
30
+ "input": {
31
+ "properties": { "text": { "type": "string" } },
32
+ "required": ["text"],
33
+ "type": "object"
34
+ },
35
+ "output": {
36
+ "properties": { "text": { "type": "string" } },
37
+ "required": ["text"],
38
+ "type": "object"
39
+ },
40
+ "readOnly": true
41
+ },
42
+ "fixture.flags": {
43
+ "description": "Return the input. The input has one property for each kind of command-line flag.",
44
+ "audience": ["cli"],
45
+ "input": {
46
+ "properties": {
47
+ "count": { "type": "integer" },
48
+ "extra": { "type": "object" },
49
+ "label": { "type": "string" },
50
+ "mode": {
51
+ "enum": ["fast", "slow"],
52
+ "type": "string"
53
+ },
54
+ "ratio": { "type": "number" },
55
+ "verbose": { "type": "boolean" }
56
+ },
57
+ "required": ["mode"],
58
+ "type": "object"
59
+ },
60
+ "output": {
61
+ "properties": {
62
+ "count": { "type": "integer" },
63
+ "extra": { "type": "object" },
64
+ "label": { "type": "string" },
65
+ "mode": {
66
+ "enum": ["fast", "slow"],
67
+ "type": "string"
68
+ },
69
+ "ratio": { "type": "number" },
70
+ "verbose": { "type": "boolean" }
71
+ },
72
+ "required": ["mode"],
73
+ "type": "object"
74
+ }
75
+ },
76
+ "fixture.hidden": {
77
+ "description": "Only the app target can use this capability.",
78
+ "audience": ["app"]
79
+ },
80
+ "fixture.refuse": {
81
+ "description": "Always return the failure Refused.",
82
+ "audience": [
83
+ "app",
84
+ "mcp",
85
+ "cli",
86
+ "api"
87
+ ],
88
+ "failures": { "Refused": {
89
+ "properties": {
90
+ "_tag": {
91
+ "const": "Refused",
92
+ "type": "string"
93
+ },
94
+ "reason": { "type": "string" }
95
+ },
96
+ "required": ["_tag", "reason"],
97
+ "type": "object"
98
+ } }
99
+ },
100
+ "fixture.slow": {
101
+ "description": "Wait for some milliseconds, then return null.",
102
+ "audience": [
103
+ "app",
104
+ "mcp",
105
+ "cli",
106
+ "api"
107
+ ],
108
+ "input": {
109
+ "properties": { "milliseconds": { "type": "number" } },
110
+ "required": ["milliseconds"],
111
+ "type": "object"
112
+ }
113
+ },
114
+ "fixture.switch.set": {
115
+ "description": "Turn the switch on or off. The switch controls the availability of fixture.toggle.",
116
+ "audience": [
117
+ "app",
118
+ "mcp",
119
+ "cli",
120
+ "api"
121
+ ],
122
+ "input": {
123
+ "properties": { "on": { "type": "boolean" } },
124
+ "required": ["on"],
125
+ "type": "object"
126
+ }
127
+ },
128
+ "fixture.toggle": {
129
+ "description": "Available only while the switch is on.",
130
+ "audience": [
131
+ "app",
132
+ "mcp",
133
+ "cli",
134
+ "api"
135
+ ]
136
+ }
137
+ }
138
+ };
139
+ //#endregion
140
+ export { fixture_acidcat_default as default };
@@ -0,0 +1,4 @@
1
+ //#region package.json
2
+ var version = "0.3.0";
3
+ //#endregion
4
+ export { version };