tactus-mcp 0.1.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/dist/safety.js ADDED
@@ -0,0 +1,216 @@
1
+ import { TactusError } from "./types.js";
2
+ /**
3
+ * Per-device rate limiter with trailing coalescing: commands under the cap go
4
+ * straight through; bursts above it collapse to the latest value, flushed at
5
+ * the rate boundary. Protects BLE from being flooded (spec §7.4).
6
+ */
7
+ class RateLimiter {
8
+ minIntervalMs;
9
+ lastSentAt = 0;
10
+ pending;
11
+ timer;
12
+ constructor(minIntervalMs) {
13
+ this.minIntervalMs = minIntervalMs;
14
+ }
15
+ submit(action) {
16
+ const now = Date.now();
17
+ const elapsed = now - this.lastSentAt;
18
+ if (elapsed >= this.minIntervalMs) {
19
+ this.lastSentAt = now;
20
+ return action(); // under the cap — await and propagate errors
21
+ }
22
+ // Over the cap: keep only the latest command, flush when the window opens.
23
+ this.pending = action;
24
+ if (!this.timer) {
25
+ this.timer = setTimeout(() => {
26
+ this.timer = undefined;
27
+ const next = this.pending;
28
+ this.pending = undefined;
29
+ if (next) {
30
+ this.lastSentAt = Date.now();
31
+ // Fire-and-forget: the caller already received a resolved promise when
32
+ // this command was coalesced, so a rejection here cannot propagate back.
33
+ // Swallow it (logged to stderr) rather than letting it surface as an
34
+ // unhandled rejection — a dropped trailing command must NEVER crash the
35
+ // server (safety is the headline feature; playbook §3).
36
+ void next().catch((e) => {
37
+ console.error(`[tactus] coalesced command failed after the rate-limit window: ${e instanceof Error ? e.message : String(e)}`);
38
+ });
39
+ }
40
+ }, this.minIntervalMs - elapsed);
41
+ this.timer.unref?.();
42
+ }
43
+ return Promise.resolve();
44
+ }
45
+ dispose() {
46
+ if (this.timer)
47
+ clearTimeout(this.timer);
48
+ this.timer = undefined;
49
+ this.pending = undefined;
50
+ }
51
+ }
52
+ /**
53
+ * Wraps a DeviceController with the safety guarantees that are Tactus's headline
54
+ * feature (spec §7.4, playbook §3): intensity clamp, an INDEPENDENT watchdog
55
+ * timer (not check-on-next-call), per-device rate limiting, stop-preemptible
56
+ * patterns, and fail-safe stops that retry rather than assume success.
57
+ */
58
+ export class SafetyLayer {
59
+ controller;
60
+ config;
61
+ watchdogs = new Map();
62
+ limiters = new Map();
63
+ patterns = new Map();
64
+ constructor(controller, config) {
65
+ this.controller = controller;
66
+ this.config = config;
67
+ }
68
+ /** Drive methods return the EFFECTIVE value actually applied (post-clamp), so
69
+ * callers can see when a request was clamped — honesty over optimism. */
70
+ async vibrate(deviceId, intensity, target) {
71
+ return this.driveScalar(deviceId, "vibrate", intensity, target);
72
+ }
73
+ async oscillate(deviceId, intensity, target) {
74
+ return this.driveScalar(deviceId, "oscillate", intensity, target);
75
+ }
76
+ async rotate(deviceId, speed, clockwise, target) {
77
+ const v = this.clampIntensity(speed);
78
+ this.cancelPattern(deviceId);
79
+ this.armWatchdog(deviceId);
80
+ await this.submit(deviceId, () => this.controller.rotate(deviceId, v, clockwise, target));
81
+ return v;
82
+ }
83
+ async linear(deviceId, position, durationMs, target) {
84
+ const p = Math.max(0, Math.min(1, position)); // position is a coordinate, not an intensity
85
+ this.cancelPattern(deviceId);
86
+ this.armWatchdog(deviceId);
87
+ await this.submit(deviceId, () => this.controller.linear(deviceId, p, durationMs, target));
88
+ return p;
89
+ }
90
+ /**
91
+ * Kick off a server-side timed pattern and return immediately. Playing is
92
+ * driven by chained timers (NOT an awaited sleep), so a single tool call
93
+ * never occupies the serial stdio handler — stop_* can always preempt it
94
+ * (playbook §3.4).
95
+ */
96
+ startVibratePattern(deviceId, steps, repeat, target) {
97
+ this.cancelPattern(deviceId);
98
+ const flat = [];
99
+ for (let r = 0; r < repeat; r++)
100
+ flat.push(...steps);
101
+ const run = { cancelled: false };
102
+ this.patterns.set(deviceId, run);
103
+ let i = 0;
104
+ const next = () => {
105
+ if (run.cancelled)
106
+ return;
107
+ if (i >= flat.length) {
108
+ this.patterns.delete(deviceId);
109
+ void this.controller.stopDevice(deviceId).catch(() => { });
110
+ return;
111
+ }
112
+ const step = flat[i++];
113
+ this.armWatchdog(deviceId);
114
+ void this.submit(deviceId, () => this.controller.output(deviceId, "vibrate", this.clampIntensity(step.intensity), target));
115
+ run.timer = setTimeout(next, step.durationMs);
116
+ run.timer.unref?.();
117
+ };
118
+ next();
119
+ }
120
+ /** Fail-safe stop: if we cannot confirm the device stopped, retry — never
121
+ * optimistically report success (playbook §3.1). */
122
+ async stopDevice(deviceId) {
123
+ this.clearWatchdog(deviceId);
124
+ this.cancelPattern(deviceId);
125
+ await this.retry(() => this.controller.stopDevice(deviceId));
126
+ }
127
+ async stopAll() {
128
+ for (const id of [...this.watchdogs.keys()])
129
+ this.clearWatchdog(id);
130
+ for (const id of [...this.patterns.keys()])
131
+ this.cancelPattern(id);
132
+ await this.retry(() => this.controller.stopAll());
133
+ }
134
+ /** Drop all per-device state for a device that has disconnected. No point
135
+ * keeping a watchdog/pattern/limiter alive for a device that is gone. */
136
+ forgetDevice(deviceId) {
137
+ this.clearWatchdog(deviceId);
138
+ this.cancelPattern(deviceId);
139
+ const limiter = this.limiters.get(deviceId);
140
+ if (limiter) {
141
+ limiter.dispose();
142
+ this.limiters.delete(deviceId);
143
+ }
144
+ }
145
+ /** Best-effort emergency stop for shutdown paths; never throws. */
146
+ async shutdown() {
147
+ for (const l of this.limiters.values())
148
+ l.dispose();
149
+ this.limiters.clear();
150
+ try {
151
+ await this.stopAll();
152
+ }
153
+ catch {
154
+ /* shutting down — nothing more we can do here */
155
+ }
156
+ }
157
+ // --- internals ---
158
+ async driveScalar(deviceId, type, intensity, target) {
159
+ const v = this.clampIntensity(intensity);
160
+ this.cancelPattern(deviceId);
161
+ this.armWatchdog(deviceId);
162
+ await this.submit(deviceId, () => this.controller.output(deviceId, type, v, target));
163
+ return v;
164
+ }
165
+ clampIntensity(v) {
166
+ return Math.max(0, Math.min(this.config.maxIntensity, v));
167
+ }
168
+ submit(deviceId, action) {
169
+ let limiter = this.limiters.get(deviceId);
170
+ if (!limiter) {
171
+ limiter = new RateLimiter(1000 / this.config.maxCommandsPerSec);
172
+ this.limiters.set(deviceId, limiter);
173
+ }
174
+ return limiter.submit(action);
175
+ }
176
+ /** (Re)arm the independent auto-stop timer for a device. */
177
+ armWatchdog(deviceId) {
178
+ this.clearWatchdog(deviceId);
179
+ const timer = setTimeout(() => {
180
+ this.watchdogs.delete(deviceId);
181
+ void this.retry(() => this.controller.stopDevice(deviceId)).catch(() => { });
182
+ }, this.config.maxContinuousMs);
183
+ timer.unref?.();
184
+ this.watchdogs.set(deviceId, timer);
185
+ }
186
+ clearWatchdog(deviceId) {
187
+ const timer = this.watchdogs.get(deviceId);
188
+ if (timer) {
189
+ clearTimeout(timer);
190
+ this.watchdogs.delete(deviceId);
191
+ }
192
+ }
193
+ cancelPattern(deviceId) {
194
+ const run = this.patterns.get(deviceId);
195
+ if (run) {
196
+ run.cancelled = true;
197
+ if (run.timer)
198
+ clearTimeout(run.timer);
199
+ this.patterns.delete(deviceId);
200
+ }
201
+ }
202
+ async retry(fn, attempts = 3) {
203
+ let lastErr;
204
+ for (let i = 0; i < attempts; i++) {
205
+ try {
206
+ await fn();
207
+ return;
208
+ }
209
+ catch (e) {
210
+ lastErr = e;
211
+ }
212
+ }
213
+ throw new TactusError(`Could not confirm stop after ${attempts} attempts: ${String(lastErr)}`, "DEVICE_ERROR");
214
+ }
215
+ }
216
+ //# sourceMappingURL=safety.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safety.js","sourceRoot":"","sources":["../src/safety.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { DeviceController } from "./controller.js";
3
+ import { SafetyLayer } from "./safety.js";
4
+ import { TactusConfig } from "./types.js";
5
+ /** Register every Tactus tool on the given server. */
6
+ export declare function registerTools(server: McpServer, safety: SafetyLayer, controller: DeviceController, config: TactusConfig): void;
7
+ export declare function makeServer(version: string): McpServer;
8
+ /** Server-side hard bounds for patterns — never trust client schema alone (playbook §4). */
9
+ export declare function enforcePatternBounds(steps: readonly {
10
+ duration_ms: number;
11
+ }[], repeat: number, config: TactusConfig): void;
package/dist/server.js ADDED
@@ -0,0 +1,234 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { TactusError } from "./types.js";
4
+ /** Server-level guidance; some clients ignore this, hence the per-tool prefixes too. */
5
+ const INSTRUCTIONS = "Tactus is a content-agnostic hardware control layer. It forwards device " +
6
+ "control commands only and generates no content. Always confirm a device " +
7
+ "exists via list_devices before driving it. Intensity is normalized 0.0-1.0. " +
8
+ "Use stop_all to immediately halt everything.";
9
+ const DRIVE_PREFIX = "[Hardware control] Forwards a command to physical hardware. ";
10
+ const text = (s) => [{ type: "text", text: s }];
11
+ const ok = (message, structured) => ({
12
+ content: text(message),
13
+ ...(structured ? { structuredContent: structured } : {}),
14
+ });
15
+ const fail = (e) => {
16
+ const message = e instanceof TactusError ? e.message : `Unexpected error: ${String(e)}`;
17
+ return { content: text(message), isError: true };
18
+ };
19
+ const deviceId = z.number().int().nonnegative().describe("Device id from list_devices");
20
+ const unit = (label) => z.number().min(0).max(1).describe(`${label}, 0.0-1.0`);
21
+ const actuatorIndex = z
22
+ .number()
23
+ .int()
24
+ .nonnegative()
25
+ .optional()
26
+ .describe("Restrict to one actuator index; omit to drive all matching actuators");
27
+ /** Register every Tactus tool on the given server. */
28
+ export function registerTools(server, safety, controller, config) {
29
+ // --- discovery / status ---
30
+ server.registerTool("list_devices", {
31
+ title: "List devices",
32
+ description: "List currently connected devices and their actuators.",
33
+ inputSchema: {},
34
+ }, async () => {
35
+ try {
36
+ const devices = controller.listDevices();
37
+ return ok(`${devices.length} device(s) connected.`, { devices, status: controller.status() });
38
+ }
39
+ catch (e) {
40
+ return fail(e);
41
+ }
42
+ });
43
+ server.registerTool("server_status", {
44
+ title: "Server status",
45
+ description: "Report connection status to Intiface and device count.",
46
+ inputSchema: {},
47
+ }, async () => {
48
+ try {
49
+ return ok("Status.", { status: controller.status() });
50
+ }
51
+ catch (e) {
52
+ return fail(e);
53
+ }
54
+ });
55
+ server.registerTool("scan_for_devices", {
56
+ title: "Scan for devices",
57
+ description: "Scan for new devices for a duration, then return any newly found.",
58
+ inputSchema: { duration_ms: z.number().int().positive().optional() },
59
+ }, async ({ duration_ms }) => {
60
+ try {
61
+ const found = await controller.scan(duration_ms ?? config.scanDefaultMs);
62
+ return ok(`Found ${found.length} new device(s).`, { devices: found });
63
+ }
64
+ catch (e) {
65
+ return fail(e);
66
+ }
67
+ });
68
+ server.registerTool("get_battery", {
69
+ title: "Get battery",
70
+ description: "Read battery level (0.0-1.0) for a device that reports it.",
71
+ inputSchema: { device_id: deviceId },
72
+ }, async ({ device_id }) => {
73
+ try {
74
+ const battery = await controller.getBattery(device_id);
75
+ return ok(`Battery: ${Math.round(battery * 100)}%`, { device_id, battery });
76
+ }
77
+ catch (e) {
78
+ return fail(e);
79
+ }
80
+ });
81
+ // --- drive (all clamped + watchdog-tracked via the safety layer) ---
82
+ server.registerTool("vibrate", {
83
+ title: "Vibrate",
84
+ description: DRIVE_PREFIX + "Set vibration intensity on a device.",
85
+ inputSchema: { device_id: deviceId, intensity: unit("Vibration intensity"), actuator_index: actuatorIndex },
86
+ }, async ({ device_id, intensity, actuator_index }) => {
87
+ try {
88
+ const effective = await safety.vibrate(device_id, intensity, { actuatorIndex: actuator_index });
89
+ return ok(driveMsg("Vibrating", device_id, intensity, effective), {
90
+ device_id,
91
+ intensity: effective,
92
+ requested: intensity,
93
+ });
94
+ }
95
+ catch (e) {
96
+ return fail(e);
97
+ }
98
+ });
99
+ server.registerTool("oscillate", {
100
+ title: "Oscillate",
101
+ description: DRIVE_PREFIX + "Set oscillation intensity on a device.",
102
+ inputSchema: { device_id: deviceId, intensity: unit("Oscillation intensity"), actuator_index: actuatorIndex },
103
+ }, async ({ device_id, intensity, actuator_index }) => {
104
+ try {
105
+ const effective = await safety.oscillate(device_id, intensity, { actuatorIndex: actuator_index });
106
+ return ok(driveMsg("Oscillating", device_id, intensity, effective), {
107
+ device_id,
108
+ intensity: effective,
109
+ requested: intensity,
110
+ });
111
+ }
112
+ catch (e) {
113
+ return fail(e);
114
+ }
115
+ });
116
+ server.registerTool("rotate", {
117
+ title: "Rotate",
118
+ description: DRIVE_PREFIX + "Set rotation speed (and direction) on a device.",
119
+ inputSchema: {
120
+ device_id: deviceId,
121
+ speed: unit("Rotation speed"),
122
+ clockwise: z.boolean().optional().describe("Direction; honored only if the device supports it"),
123
+ actuator_index: actuatorIndex,
124
+ },
125
+ }, async ({ device_id, speed, clockwise, actuator_index }) => {
126
+ try {
127
+ const effective = await safety.rotate(device_id, speed, clockwise ?? true, { actuatorIndex: actuator_index });
128
+ return ok(driveMsg("Rotating", device_id, speed, effective), {
129
+ device_id,
130
+ speed: effective,
131
+ requested: speed,
132
+ clockwise: clockwise ?? true,
133
+ });
134
+ }
135
+ catch (e) {
136
+ return fail(e);
137
+ }
138
+ });
139
+ server.registerTool("linear", {
140
+ title: "Linear stroke",
141
+ description: DRIVE_PREFIX + "Move a stroker to a position over a duration.",
142
+ inputSchema: {
143
+ device_id: deviceId,
144
+ position: unit("Target position"),
145
+ duration_ms: z.number().int().positive().describe("Time to reach the position, ms"),
146
+ actuator_index: actuatorIndex,
147
+ },
148
+ }, async ({ device_id, position, duration_ms, actuator_index }) => {
149
+ try {
150
+ const effective = await safety.linear(device_id, position, duration_ms, { actuatorIndex: actuator_index });
151
+ return ok(`Moving device ${device_id} to ${pct(effective)} over ${duration_ms}ms.`, {
152
+ device_id,
153
+ position: effective,
154
+ duration_ms,
155
+ });
156
+ }
157
+ catch (e) {
158
+ return fail(e);
159
+ }
160
+ });
161
+ server.registerTool("vibrate_pattern", {
162
+ title: "Vibrate pattern",
163
+ description: DRIVE_PREFIX + "Play a timed multi-step vibration pattern; interruptible by any stop.",
164
+ inputSchema: {
165
+ device_id: deviceId,
166
+ steps: z
167
+ .array(z.object({ intensity: unit("Step intensity"), duration_ms: z.number().int().positive() }))
168
+ .min(1),
169
+ repeat: z.number().int().positive().optional(),
170
+ actuator_index: actuatorIndex,
171
+ },
172
+ }, async ({ device_id, steps, repeat, actuator_index }) => {
173
+ try {
174
+ const count = repeat ?? 1;
175
+ enforcePatternBounds(steps, count, config);
176
+ const pattern = steps.map((s) => ({ intensity: s.intensity, durationMs: s.duration_ms }));
177
+ safety.startVibratePattern(device_id, pattern, count, { actuatorIndex: actuator_index });
178
+ return ok(`Started pattern on device ${device_id} (${steps.length} steps x${count}).`, {
179
+ device_id,
180
+ steps: steps.length,
181
+ repeat: count,
182
+ });
183
+ }
184
+ catch (e) {
185
+ return fail(e);
186
+ }
187
+ });
188
+ // --- stop (always available, never clamped away) ---
189
+ server.registerTool("stop_device", {
190
+ title: "Stop device",
191
+ description: "Stop all actuators on one device.",
192
+ inputSchema: { device_id: deviceId },
193
+ }, async ({ device_id }) => {
194
+ try {
195
+ await safety.stopDevice(device_id);
196
+ return ok(`Stopped device ${device_id}.`, { device_id, stopped: true });
197
+ }
198
+ catch (e) {
199
+ return fail(e);
200
+ }
201
+ });
202
+ server.registerTool("stop_all", {
203
+ title: "Stop all (emergency)",
204
+ description: "Immediately stop every connected device. Emergency stop.",
205
+ inputSchema: {},
206
+ }, async () => {
207
+ try {
208
+ await safety.stopAll();
209
+ return ok("Stopped all devices.", { stopped: true });
210
+ }
211
+ catch (e) {
212
+ return fail(e);
213
+ }
214
+ });
215
+ }
216
+ export function makeServer(version) {
217
+ return new McpServer({ name: "tactus", version }, { instructions: INSTRUCTIONS });
218
+ }
219
+ const pct = (v) => `${Math.round(v * 100)}%`;
220
+ /** Human-readable drive message that notes when the request was clamped. */
221
+ const driveMsg = (verb, deviceId, requested, effective) => effective < requested
222
+ ? `${verb} device ${deviceId} at ${pct(effective)} (clamped from ${pct(requested)} by MAX_INTENSITY).`
223
+ : `${verb} device ${deviceId} at ${pct(effective)}.`;
224
+ /** Server-side hard bounds for patterns — never trust client schema alone (playbook §4). */
225
+ export function enforcePatternBounds(steps, repeat, config) {
226
+ if (steps.length > config.maxPatternSteps) {
227
+ throw new TactusError(`Pattern has ${steps.length} steps; max is ${config.maxPatternSteps}.`, "INVALID_INPUT");
228
+ }
229
+ const total = steps.reduce((sum, s) => sum + s.duration_ms, 0) * repeat;
230
+ if (total > config.maxContinuousMs) {
231
+ throw new TactusError(`Pattern total duration ${total}ms exceeds the ${config.maxContinuousMs}ms limit.`, "INVALID_INPUT");
232
+ }
233
+ }
234
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ /** Shared, protocol-agnostic types for Tactus. */
2
+ export type ActuatorType = "vibrate" | "oscillate" | "rotate" | "linear" | "constrict" | "inflate" | "unknown";
3
+ /** A single drivable output on a device (maps to one Buttplug feature output). */
4
+ export interface ActuatorInfo {
5
+ index: number;
6
+ type: ActuatorType;
7
+ /** Number of discrete steps the actuator accepts (from the feature value range). */
8
+ step_count: number;
9
+ }
10
+ export interface DeviceInfo {
11
+ id: number;
12
+ name: string;
13
+ actuators: ActuatorInfo[];
14
+ has_battery: boolean;
15
+ /** 0..1, present only when reported. */
16
+ battery?: number;
17
+ }
18
+ export interface ServerStatus {
19
+ connected: boolean;
20
+ intiface_url: string;
21
+ server_name?: string;
22
+ device_count: number;
23
+ }
24
+ export interface TactusConfig {
25
+ intifaceUrl: string;
26
+ /** Clamp ceiling for drive intensity, 0..1. */
27
+ maxIntensity: number;
28
+ /** Watchdog auto-stop after this many ms of uninterrupted drive. */
29
+ maxContinuousMs: number;
30
+ scanDefaultMs: number;
31
+ /** Per-device command rate cap; excess is coalesced. */
32
+ maxCommandsPerSec: number;
33
+ maxPatternSteps: number;
34
+ /** When true, the intensity clamp is disabled (explicitly gated, discouraged). */
35
+ allowUnsafe: boolean;
36
+ }
37
+ /** Codes for actionable, non-crashing failures surfaced to callers. */
38
+ export type TactusErrorCode = "NOT_CONNECTED" | "UNKNOWN_DEVICE" | "NO_ACTUATOR" | "INVALID_INPUT" | "DEVICE_ERROR";
39
+ export declare class TactusError extends Error {
40
+ readonly code: TactusErrorCode;
41
+ constructor(message: string, code: TactusErrorCode);
42
+ }
package/dist/types.js ADDED
@@ -0,0 +1,10 @@
1
+ /** Shared, protocol-agnostic types for Tactus. */
2
+ export class TactusError extends Error {
3
+ code;
4
+ constructor(message, code) {
5
+ super(message);
6
+ this.code = code;
7
+ this.name = "TactusError";
8
+ }
9
+ }
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAyDlD,MAAM,OAAO,WAAY,SAAQ,KAAK;IAGzB;IAFX,YACE,OAAe,EACN,IAAqB;QAE9B,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,SAAI,GAAJ,IAAI,CAAiB;QAG9B,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "tactus-mcp",
3
+ "version": "0.1.1",
4
+ "description": "A content-agnostic MCP server that exposes intimate hardware control to AI agents by wrapping Buttplug/Intiface.",
5
+ "type": "module",
6
+ "bin": {
7
+ "tactus-mcp": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "CHANGELOG.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "rm -rf dist && tsc",
20
+ "typecheck": "tsc --noEmit",
21
+ "m0": "tsx src/m0-smoke.ts",
22
+ "e2e": "tsx src/e2e-probe.ts",
23
+ "test": "node --import tsx --test test/*.test.ts",
24
+ "start": "node dist/index.js"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "buttplug",
29
+ "intiface",
30
+ "haptics"
31
+ ],
32
+ "author": "ProjectAILiberation",
33
+ "license": "Apache-2.0",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/ProjectAILiberation/tactus.git"
37
+ },
38
+ "homepage": "https://github.com/ProjectAILiberation/tactus#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/ProjectAILiberation/tactus/issues"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "registry": "https://registry.npmjs.org"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "buttplug": "^5.0.1",
49
+ "zod": "^4.4.3"
50
+ },
51
+ "devDependencies": {
52
+ "@types/node": "^25.9.3",
53
+ "tsx": "^4.22.4",
54
+ "typescript": "^6.0.3"
55
+ }
56
+ }