touchdesigner-mcp-server 1.3.0 → 1.4.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,276 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createErrorResult, createSuccessResult } from "../core/result.js";
3
+ import { DEFAULT_SESSION_CONFIG } from "./config.js";
4
+ /**
5
+ * Session Manager
6
+ *
7
+ * Manages client sessions for Streamable HTTP transport.
8
+ * Provides session creation, validation, TTL-based expiration, and automatic cleanup.
9
+ *
10
+ * Note: Session validation (checking if session ID exists) is now handled by
11
+ * StreamableHTTPServerTransport.handleRequest(). This SessionManager focuses on:
12
+ * - Session creation via SDK callbacks (onsessioninitialized)
13
+ * - Session cleanup via SDK callbacks (onsessionclosed) and TTL-based cleanup
14
+ * - Session tracking for health checks and monitoring
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * const sessionManager = new SessionManager(
19
+ * { enabled: true, ttl: 60 * 60 * 1000 }, // 1 hour TTL
20
+ * logger
21
+ * );
22
+ *
23
+ * // Create session (typically called from SDK callback)
24
+ * const sessionId = sessionManager.create({ clientVersion: '1.0' });
25
+ *
26
+ * // Start automatic TTL-based cleanup
27
+ * sessionManager.startTTLCleanup();
28
+ *
29
+ * // Stop cleanup when done
30
+ * sessionManager.stopTTLCleanup();
31
+ * ```
32
+ */
33
+ export class SessionManager {
34
+ sessions = new Map();
35
+ config;
36
+ logger;
37
+ cleanupInterval = null;
38
+ onSessionExpired = null;
39
+ constructor(config, logger) {
40
+ // Apply defaults for optional values to ensure TTL cleanup is active by default
41
+ this.config = {
42
+ cleanupInterval: config.cleanupInterval ?? DEFAULT_SESSION_CONFIG.cleanupInterval,
43
+ enabled: config.enabled ?? DEFAULT_SESSION_CONFIG.enabled,
44
+ ttl: config.ttl ?? DEFAULT_SESSION_CONFIG.ttl,
45
+ };
46
+ this.logger = logger;
47
+ }
48
+ /**
49
+ * Create a new session with optional metadata
50
+ *
51
+ * @param metadata - Optional metadata to associate with the session
52
+ * @returns Session ID (UUID v4)
53
+ */
54
+ create(metadata) {
55
+ const sessionId = randomUUID();
56
+ const now = Date.now();
57
+ const session = {
58
+ createdAt: now,
59
+ id: sessionId,
60
+ lastAccessedAt: now,
61
+ metadata,
62
+ };
63
+ this.sessions.set(sessionId, session);
64
+ this.logger.sendLog({
65
+ data: `Session created: ${sessionId}${metadata ? ` (metadata: ${JSON.stringify(metadata)})` : ""}`,
66
+ level: "info",
67
+ logger: "SessionManager",
68
+ });
69
+ return sessionId;
70
+ }
71
+ /**
72
+ * Update lastAccessedAt when a session receives activity
73
+ *
74
+ * @param sessionId - Session ID to refresh
75
+ */
76
+ touch(sessionId) {
77
+ const session = this.sessions.get(sessionId);
78
+ if (!session) {
79
+ return createErrorResult(new Error(`Session not found: ${sessionId}`));
80
+ }
81
+ session.lastAccessedAt = Date.now();
82
+ return createSuccessResult(undefined);
83
+ }
84
+ /**
85
+ * Register a callback to run when TTL cleanup expires a session.
86
+ *
87
+ * @param handler - Callback invoked with expired session ID
88
+ */
89
+ setExpirationHandler(handler) {
90
+ this.onSessionExpired = handler;
91
+ }
92
+ /**
93
+ * Register an SDK-created session for tracking
94
+ *
95
+ * This method registers sessions that were created by the MCP SDK's
96
+ * StreamableHTTPServerTransport. The SDK generates session IDs via the
97
+ * sessionIdGenerator callback, and then calls onsessioninitialized.
98
+ * This method creates a new tracking entry with current timestamps
99
+ * so the session can be managed for TTL cleanup and monitoring.
100
+ *
101
+ * @param sessionId - Session ID generated by SDK
102
+ * @param metadata - Optional metadata to associate with the session
103
+ */
104
+ register(sessionId, metadata) {
105
+ const now = Date.now();
106
+ const session = {
107
+ createdAt: now,
108
+ id: sessionId,
109
+ lastAccessedAt: now,
110
+ metadata,
111
+ };
112
+ this.sessions.set(sessionId, session);
113
+ this.logger.sendLog({
114
+ data: `Session registered: ${sessionId}${metadata ? ` (metadata: ${JSON.stringify(metadata)})` : ""}`,
115
+ level: "info",
116
+ logger: "SessionManager",
117
+ });
118
+ }
119
+ /**
120
+ * Clean up (delete) a session by ID
121
+ *
122
+ * @param sessionId - Session ID to clean up
123
+ * @returns Result indicating success or failure
124
+ */
125
+ cleanup(sessionId) {
126
+ const existed = this.sessions.delete(sessionId);
127
+ if (existed) {
128
+ this.logger.sendLog({
129
+ data: `Session cleaned up: ${sessionId}`,
130
+ level: "info",
131
+ logger: "SessionManager",
132
+ });
133
+ return createSuccessResult(undefined);
134
+ }
135
+ return createErrorResult(new Error(`Session not found for cleanup: ${sessionId}`));
136
+ }
137
+ /**
138
+ * List all active sessions
139
+ *
140
+ * @returns Array of all active sessions
141
+ */
142
+ list() {
143
+ return Array.from(this.sessions.values());
144
+ }
145
+ /**
146
+ * Start automatic TTL-based cleanup
147
+ *
148
+ * Runs cleanup task at an interval of TTL/2 to remove expired sessions.
149
+ * Does nothing if TTL is not configured or cleanup is already running.
150
+ */
151
+ startTTLCleanup() {
152
+ // Don't start if TTL not configured or already running
153
+ if (!this.config.ttl || this.cleanupInterval) {
154
+ return;
155
+ }
156
+ const intervalMs = this.config.cleanupInterval || this.config.ttl / 2;
157
+ this.logger.sendLog({
158
+ data: `Starting TTL cleanup (interval: ${intervalMs}ms, TTL: ${this.config.ttl}ms)`,
159
+ level: "info",
160
+ logger: "SessionManager",
161
+ });
162
+ this.cleanupInterval = setInterval(() => {
163
+ try {
164
+ this.runCleanupTask();
165
+ }
166
+ catch (error) {
167
+ const err = error instanceof Error ? error : new Error(String(error));
168
+ this.logger.sendLog({
169
+ data: `CRITICAL: TTL cleanup task failed: ${err.message}. Stack: ${err.stack}`,
170
+ level: "error",
171
+ logger: "SessionManager",
172
+ });
173
+ }
174
+ }, intervalMs);
175
+ // Don't keep the process alive just for cleanup
176
+ this.cleanupInterval.unref();
177
+ }
178
+ /**
179
+ * Stop automatic TTL-based cleanup
180
+ */
181
+ stopTTLCleanup() {
182
+ if (this.cleanupInterval) {
183
+ clearInterval(this.cleanupInterval);
184
+ this.cleanupInterval = null;
185
+ this.logger.sendLog({
186
+ data: "Stopped TTL cleanup",
187
+ level: "info",
188
+ logger: "SessionManager",
189
+ });
190
+ }
191
+ }
192
+ /**
193
+ * Run a single cleanup task to remove expired sessions
194
+ *
195
+ * @private
196
+ */
197
+ runCleanupTask() {
198
+ const ttl = this.config.ttl;
199
+ if (!ttl) {
200
+ return;
201
+ }
202
+ const now = Date.now();
203
+ const expiredIds = [];
204
+ for (const [id, session] of this.sessions.entries()) {
205
+ try {
206
+ const elapsed = now - session.lastAccessedAt;
207
+ if (elapsed > ttl) {
208
+ expiredIds.push(id);
209
+ }
210
+ }
211
+ catch (error) {
212
+ // Log individual session cleanup errors but continue processing others
213
+ const err = error instanceof Error ? error : new Error(String(error));
214
+ this.logger.sendLog({
215
+ data: `Error cleaning session ${id}: ${err.message}`,
216
+ level: "error",
217
+ logger: "SessionManager",
218
+ });
219
+ }
220
+ }
221
+ for (const sessionId of expiredIds) {
222
+ try {
223
+ const result = this.onSessionExpired?.(sessionId);
224
+ if (result && typeof result.catch === "function") {
225
+ result.catch((error) => {
226
+ const err = error instanceof Error ? error : new Error(String(error));
227
+ this.logger.sendLog({
228
+ data: `Error running expiration handler for ${sessionId}: ${err.message}`,
229
+ level: "error",
230
+ logger: "SessionManager",
231
+ });
232
+ });
233
+ }
234
+ }
235
+ catch (error) {
236
+ const err = error instanceof Error ? error : new Error(String(error));
237
+ this.logger.sendLog({
238
+ data: `Error running expiration handler for ${sessionId}: ${err.message}`,
239
+ level: "error",
240
+ logger: "SessionManager",
241
+ });
242
+ }
243
+ // Ensure the session is removed from tracking even if handler fails
244
+ this.sessions.delete(sessionId);
245
+ }
246
+ if (expiredIds.length > 0) {
247
+ this.logger.sendLog({
248
+ data: `Cleanup completed: removed ${expiredIds.length} expired session(s): ${expiredIds.join(", ")}`,
249
+ level: "info",
250
+ logger: "SessionManager",
251
+ });
252
+ }
253
+ }
254
+ /**
255
+ * Get number of active sessions
256
+ *
257
+ * @returns Number of active sessions
258
+ */
259
+ getActiveSessionCount() {
260
+ return this.sessions.size;
261
+ }
262
+ /**
263
+ * Clear all sessions
264
+ *
265
+ * Useful for testing or emergency cleanup.
266
+ */
267
+ clearAll() {
268
+ const count = this.sessions.size;
269
+ this.sessions.clear();
270
+ this.logger.sendLog({
271
+ data: `All sessions cleared: ${count} session(s) removed`,
272
+ level: "info",
273
+ logger: "SessionManager",
274
+ });
275
+ }
276
+ }
@@ -0,0 +1,272 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4
+ import { createErrorResult, createSuccessResult } from "../core/result.js";
5
+ /**
6
+ * Transport Registry
7
+ *
8
+ * Manages per-session transport and server instances for Streamable HTTP transport.
9
+ * Each session gets its own isolated transport and server to maintain independent MCP protocol state.
10
+ *
11
+ * Key Responsibilities:
12
+ * - Create new transport + server instances for new sessions
13
+ * - Reuse existing transport for requests with valid session IDs
14
+ * - Clean up sessions on close or expiration
15
+ * - Track active session count for health checks
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const registry = new TransportRegistry(config, sessionManager, logger);
20
+ *
21
+ * // In HTTP request handler
22
+ * const transport = await registry.getOrCreate(
23
+ * sessionId,
24
+ * requestBody,
25
+ * () => createTouchDesignerServer()
26
+ * );
27
+ *
28
+ * if (transport) {
29
+ * await transport.handleRequest(req, res, requestBody);
30
+ * }
31
+ *
32
+ * // On shutdown
33
+ * await registry.cleanup();
34
+ * ```
35
+ */
36
+ export class TransportRegistry {
37
+ sessions = new Map();
38
+ config;
39
+ sessionManager;
40
+ logger;
41
+ constructor(config, sessionManager, logger) {
42
+ this.config = config;
43
+ this.sessionManager = sessionManager;
44
+ this.logger = logger;
45
+ if (this.sessionManager) {
46
+ this.sessionManager.setExpirationHandler(async (sessionId) => {
47
+ const entry = this.sessions.get(sessionId);
48
+ if (!entry) {
49
+ return;
50
+ }
51
+ this.logger.sendLog({
52
+ data: `Expiring session via TTL: ${sessionId}`,
53
+ level: "info",
54
+ logger: "TransportRegistry",
55
+ });
56
+ try {
57
+ await entry.transport.close();
58
+ }
59
+ catch (error) {
60
+ const err = error instanceof Error ? error : new Error(String(error));
61
+ this.logger.sendLog({
62
+ data: `Error closing expired session ${sessionId}: ${err.message}`,
63
+ level: "error",
64
+ logger: "TransportRegistry",
65
+ });
66
+ }
67
+ finally {
68
+ this.remove(sessionId);
69
+ }
70
+ });
71
+ }
72
+ }
73
+ /**
74
+ * Get or create transport for a session
75
+ *
76
+ * Logic:
77
+ * 1. If sessionId exists and valid → return existing transport
78
+ * 2. If no sessionId and request is initialize → create new transport + server
79
+ * 3. Otherwise → return null (invalid session)
80
+ *
81
+ * @param sessionId - Session ID from mcp-session-id header (undefined for new sessions)
82
+ * @param requestBody - JSON-RPC request body
83
+ * @param serverFactory - Factory function to create new Server instances
84
+ * @returns Transport instance or null if session is invalid
85
+ */
86
+ async getOrCreate(sessionId, requestBody, serverFactory) {
87
+ // Case 1: Reuse existing session
88
+ if (sessionId && this.sessions.has(sessionId)) {
89
+ const entry = this.sessions.get(sessionId);
90
+ if (entry) {
91
+ if (this.sessionManager) {
92
+ this.sessionManager.touch(sessionId);
93
+ }
94
+ this.logger.sendLog({
95
+ data: `Reusing existing session: ${sessionId}`,
96
+ level: "debug",
97
+ logger: "TransportRegistry",
98
+ });
99
+ return entry.transport;
100
+ }
101
+ }
102
+ // Case 2: Create new session (only for initialize requests without session ID)
103
+ if (!sessionId && isInitializeRequest(requestBody)) {
104
+ return await this.createSession(serverFactory);
105
+ }
106
+ // Case 3: Invalid session (session ID provided but not found, or non-initialize without session)
107
+ this.logger.sendLog({
108
+ data: `Invalid session request: sessionId=${sessionId}, isInitialize=${isInitializeRequest(requestBody)}`,
109
+ level: "warning",
110
+ logger: "TransportRegistry",
111
+ });
112
+ return null;
113
+ }
114
+ /**
115
+ * Create a new session with transport and server instances
116
+ *
117
+ * @param serverFactory - Factory function to create new Server instances
118
+ * @returns Transport instance for the new session
119
+ */
120
+ async createSession(serverFactory) {
121
+ let transport = null;
122
+ let server = null;
123
+ // Create transport with session lifecycle callbacks
124
+ transport = new StreamableHTTPServerTransport({
125
+ // Disable JSON responses (use SSE for streaming)
126
+ enableJsonResponse: false,
127
+ // Session close callback
128
+ onsessionclosed: (sessionId) => {
129
+ this.logger.sendLog({
130
+ data: `Session closed: ${sessionId}`,
131
+ level: "info",
132
+ logger: "TransportRegistry",
133
+ });
134
+ // Remove from registry
135
+ this.remove(sessionId);
136
+ // Cleanup from SessionManager
137
+ if (this.sessionManager) {
138
+ this.sessionManager.cleanup(sessionId);
139
+ }
140
+ },
141
+ // Session initialization callback
142
+ onsessioninitialized: (sessionId) => {
143
+ this.logger.sendLog({
144
+ data: `Session initialized: ${sessionId}`,
145
+ level: "info",
146
+ logger: "TransportRegistry",
147
+ });
148
+ // Store session in registry
149
+ if (transport && server) {
150
+ this.sessions.set(sessionId, {
151
+ createdAt: Date.now(),
152
+ server,
153
+ transport,
154
+ });
155
+ this.logger.sendLog({
156
+ data: `Session stored in registry: ${sessionId} (total: ${this.sessions.size})`,
157
+ level: "debug",
158
+ logger: "TransportRegistry",
159
+ });
160
+ }
161
+ // Register with SessionManager for TTL tracking
162
+ if (this.sessionManager) {
163
+ this.sessionManager.register(sessionId);
164
+ }
165
+ },
166
+ // Retry interval for SSE
167
+ retryInterval: this.config.retryInterval,
168
+ // Session ID generator
169
+ sessionIdGenerator: this.config.sessionConfig?.enabled
170
+ ? () => randomUUID()
171
+ : undefined,
172
+ });
173
+ // Handle transport close event
174
+ transport.onclose = () => {
175
+ if (transport?.sessionId) {
176
+ this.logger.sendLog({
177
+ data: `Transport closed for session: ${transport.sessionId}`,
178
+ level: "debug",
179
+ logger: "TransportRegistry",
180
+ });
181
+ this.remove(transport.sessionId);
182
+ }
183
+ };
184
+ // Create server instance
185
+ server = serverFactory();
186
+ // Connect server to transport
187
+ await server.connect(transport);
188
+ this.logger.sendLog({
189
+ data: "Created new session (session ID will be assigned after initialize)",
190
+ level: "info",
191
+ logger: "TransportRegistry",
192
+ });
193
+ return transport;
194
+ }
195
+ /**
196
+ * Remove session from registry
197
+ *
198
+ * @param sessionId - Session ID to remove
199
+ */
200
+ remove(sessionId) {
201
+ const entry = this.sessions.get(sessionId);
202
+ if (entry) {
203
+ this.sessions.delete(sessionId);
204
+ this.logger.sendLog({
205
+ data: `Session removed from registry: ${sessionId} (remaining: ${this.sessions.size})`,
206
+ level: "info",
207
+ logger: "TransportRegistry",
208
+ });
209
+ }
210
+ }
211
+ /**
212
+ * Get number of active sessions
213
+ *
214
+ * @returns Active session count
215
+ */
216
+ getCount() {
217
+ return this.sessions.size;
218
+ }
219
+ /**
220
+ * Get all session IDs
221
+ *
222
+ * @returns Array of session IDs
223
+ */
224
+ getSessionIds() {
225
+ return Array.from(this.sessions.keys());
226
+ }
227
+ /**
228
+ * Cleanup all sessions
229
+ *
230
+ * Called during graceful shutdown to close all active sessions
231
+ *
232
+ * @returns Result indicating success or failure
233
+ */
234
+ async cleanup() {
235
+ try {
236
+ this.logger.sendLog({
237
+ data: `Cleaning up ${this.sessions.size} active session(s)`,
238
+ level: "info",
239
+ logger: "TransportRegistry",
240
+ });
241
+ const closePromises = [];
242
+ for (const [sessionId, entry] of this.sessions.entries()) {
243
+ try {
244
+ // Close transport (this will trigger onsessionclosed callback)
245
+ closePromises.push(entry.transport.close());
246
+ }
247
+ catch (error) {
248
+ const err = error instanceof Error ? error : new Error(String(error));
249
+ this.logger.sendLog({
250
+ data: `Error closing session ${sessionId}: ${err.message}`,
251
+ level: "error",
252
+ logger: "TransportRegistry",
253
+ });
254
+ }
255
+ }
256
+ // Wait for all transports to close
257
+ await Promise.all(closePromises);
258
+ // Clear the map (should already be empty due to onsessionclosed callbacks)
259
+ this.sessions.clear();
260
+ this.logger.sendLog({
261
+ data: "All sessions cleaned up",
262
+ level: "info",
263
+ logger: "TransportRegistry",
264
+ });
265
+ return createSuccessResult(undefined);
266
+ }
267
+ catch (error) {
268
+ const err = error instanceof Error ? error : new Error(String(error));
269
+ return createErrorResult(new Error(`Failed to cleanup sessions: ${err.message}`));
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,78 @@
1
+ import { ZodError } from "zod";
2
+ import { createErrorResult, createSuccessResult } from "../core/result.js";
3
+ import { TransportConfigSchema } from "./config.js";
4
+ /**
5
+ * Transport configuration validator using Zod schemas
6
+ */
7
+ export class TransportConfigValidator {
8
+ /**
9
+ * Validate a transport configuration
10
+ *
11
+ * @param config - The configuration to validate
12
+ * @returns Result with validated config or validation errors
13
+ */
14
+ static validate(config) {
15
+ try {
16
+ const validatedConfig = TransportConfigSchema.parse(config);
17
+ return createSuccessResult(validatedConfig);
18
+ }
19
+ catch (error) {
20
+ if (error instanceof ZodError) {
21
+ const validationErrors = TransportConfigValidator.formatZodErrors(error);
22
+ const errorMessage = TransportConfigValidator.buildErrorMessage(validationErrors);
23
+ return createErrorResult(new Error(errorMessage));
24
+ }
25
+ // Unexpected error
26
+ const err = error instanceof Error ? error : new Error(String(error));
27
+ return createErrorResult(new Error(`Transport configuration validation failed: ${err.message}`));
28
+ }
29
+ }
30
+ /**
31
+ * Format Zod validation errors into structured ValidationError objects
32
+ *
33
+ * @param zodError - The Zod validation error
34
+ * @returns Array of structured validation errors
35
+ */
36
+ static formatZodErrors(zodError) {
37
+ return zodError.issues.map((err) => ({
38
+ field: err.path.join("."),
39
+ message: err.message,
40
+ }));
41
+ }
42
+ /**
43
+ * Build a comprehensive error message from validation errors
44
+ *
45
+ * @param errors - Array of validation errors
46
+ * @returns Formatted error message
47
+ */
48
+ static buildErrorMessage(errors) {
49
+ const errorLines = errors.map((err) => {
50
+ if (err.field) {
51
+ return ` - ${err.field}: ${err.message}`;
52
+ }
53
+ return ` - ${err.message}`;
54
+ });
55
+ return `Transport configuration validation failed:\n${errorLines.join("\n")}`;
56
+ }
57
+ /**
58
+ * Validate and merge with defaults for HTTP transport
59
+ * This is a convenience method for applying default values after validation
60
+ *
61
+ * @param config - The configuration to validate
62
+ * @returns Result with validated and merged config
63
+ */
64
+ static validateAndMergeDefaults(config) {
65
+ const validationResult = TransportConfigValidator.validate(config);
66
+ if (!validationResult.success) {
67
+ return validationResult;
68
+ }
69
+ // No merging needed for stdio
70
+ if (validationResult.data.type === "stdio") {
71
+ return validationResult;
72
+ }
73
+ // Merge defaults for HTTP transport
74
+ // Note: Defaults are already defined in config.ts
75
+ // This method is here for future extensibility if runtime merging is needed
76
+ return validationResult;
77
+ }
78
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "touchdesigner-mcp-server",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for TouchDesigner",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,6 +13,9 @@
13
13
  "author": "8beeeaaat",
14
14
  "license": "MIT",
15
15
  "mcpName": "io.github.8beeeaaat/touchdesigner-mcp-server",
16
+ "mcpCompatibility": {
17
+ "minApiVersion": "1.3.0"
18
+ },
16
19
  "bugs": {
17
20
  "url": "https://github.com/8beeeaaat/touchdesigner-mcp/issues"
18
21
  },
@@ -23,31 +26,35 @@
23
26
  "touchdesigner-mcp-server": "dist/cli.js"
24
27
  },
25
28
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.23.0",
29
+ "@modelcontextprotocol/sdk": "^1.24.3",
27
30
  "@mozilla/readability": "^0.6.0",
28
31
  "@types/axios": "^0.14.4",
29
32
  "@types/ws": "^8.18.1",
30
33
  "@types/yargs": "^17.0.35",
31
34
  "axios": "^1.13.2",
35
+ "express": "^5.2.1",
32
36
  "mustache": "^4.2.0",
37
+ "semver": "^7.7.3",
33
38
  "yaml": "^2.8.2",
34
39
  "zod": "4.1.13"
35
40
  },
36
41
  "devDependencies": {
37
42
  "@biomejs/biome": "2.3.8",
38
43
  "@openapitools/openapi-generator-cli": "^2.25.2",
44
+ "@types/express": "^5.0.6",
39
45
  "@types/jsdom": "^27.0.0",
40
46
  "@types/mustache": "^4.2.6",
41
47
  "@types/node": "^24.10.1",
42
- "@vitest/coverage-v8": "^4.0.14",
48
+ "@types/semver": "^7.7.1",
49
+ "@vitest/coverage-v8": "^4.0.15",
43
50
  "archiver": "^7.0.1",
44
- "msw": "^2.12.3",
51
+ "msw": "^2.12.4",
45
52
  "npm-run-all": "^4.1.5",
46
53
  "orval": "^7.17.0",
47
- "prettier": "^3.7.3",
54
+ "prettier": "^3.7.4",
48
55
  "shx": "^0.4.0",
49
56
  "typescript": "^5.9.3",
50
- "vitest": "^4.0.14"
57
+ "vitest": "^4.0.15"
51
58
  },
52
59
  "type": "module",
53
60
  "exports": {
@@ -75,12 +82,15 @@
75
82
  "format:python": "ruff format td/ && ruff check --fix td/",
76
83
  "format:yaml": "prettier --write \"**/*.{yml,yaml}\"",
77
84
  "dev": "npx @modelcontextprotocol/inspector node dist/cli.js --stdio",
85
+ "http": "npm run build && node dist/cli.js --mcp-http-port=6280 --mcp-http-host=127.0.0.1 --host=http://127.0.0.1 --port=9981",
78
86
  "test": "run-p test:*",
79
87
  "test:integration": "vitest run ./tests/integration",
80
88
  "test:unit": "vitest run ./tests/unit",
81
89
  "coverage": "vitest run --coverage",
90
+ "version": "run-p version:*",
91
+ "version:api": "node ./scripts/syncApiServerVersions.ts",
92
+ "version:mcp": "node ./scripts/syncMcpServerVersions.ts",
82
93
  "gen": "run-s gen:*",
83
- "gen:version": "node ./scripts/syncVersions.ts",
84
94
  "gen:webserver": "openapi-generator-cli generate -i ./src/api/index.yml -g python-flask -o ./td/modules/td_server",
85
95
  "gen:handlers": "node td/genHandlers.js",
86
96
  "gen:mcp": "orval --config ./orval.config.ts"