mcp-interactive-choice 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Interactive Choice MCP Server (Native UI)
2
+
3
+ A Model Context Protocol (MCP) server that allows AI agents to ask questions through a **native window** (built with Tauri), preventing context-breaking interruptions and providing a premium user experience.
4
+
5
+ ## ✨ Features
6
+
7
+ - **Native Window**: A native window appears when the agent needs your input.
8
+ - **Markdown Support**: Detailed descriptions from the AI are rendered in markdown.
9
+ - **Cross-Platform**: Supports Windows and macOS.
10
+
11
+ ## 🚀 Setup & Installation
12
+
13
+ ### Option A: Run with npx (Recommended)
14
+ You can use the server directly via `npx` in your MCP client configuration:
15
+
16
+ ```json
17
+ {
18
+ "mcpServers": {
19
+ "interactive-choice": {
20
+ "command": "npx",
21
+ "args": [
22
+ "-y",
23
+ "mcp-interactive-choice"
24
+ ]
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ ### Option B: Local Build
31
+ #### 1. Build Everything
32
+ From the project root:
33
+ ```bash
34
+ npm install
35
+ npm run build
36
+ ```
37
+ This will:
38
+ 1. Build the frontend (Vite)
39
+ 2. Build the Tauri binary (Rust)
40
+ 3. Compile the MCP server (TypeScript)
41
+ 4. Copy the binary to the `bin/` folder
42
+
43
+ #### 2. Register with your MCP Client
44
+ Update your configuration (e.g., `claude_desktop_config.json`):
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "interactive-choice": {
50
+ "command": "node",
51
+ "args": [
52
+ "/path/to/mcp-interactive-choice/dist/index.js"
53
+ ]
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ ## 🛠️ Tool: `ask_user`
60
+
61
+ The agent calls this tool when it needs a human decision.
62
+
63
+ **Arguments:**
64
+ - `title` (optional): Short headline for the question.
65
+ - `body` (optional): Detailed context in Markdown.
66
+ - `choices` (required): List of strings.
67
+ - `recommended` (optional): One of the strings from `choices` that the agent suggests.
68
+ - `allowCustom` (optional): Boolean to show a text box for custom input.
69
+ - `timeoutSec` (optional): How long to wait (in seconds) before the tool auto-fails.
70
+
71
+ **Response:**
72
+ - Returns the string value of the selected choice.
73
+ - Returns the custom text if provided.
74
+ - Returns `"user cancelled the selection"` if the window is closed manually.
75
+
76
+ ## 🛠️ Development & Debugging
77
+
78
+ ### UI Development (Hot Reloading)
79
+ To iterate on the UI with hot reloading:
80
+ 1. Go to `packages/native-ui`.
81
+ 2. Run `npm run tauri dev`.
82
+
83
+ **Running with CLI parameters (Windows/PowerShell):**
84
+ To test specific data during development, use the flag `--input`:
85
+ ```powershell
86
+ npm run tauri dev -- -- -- --input '{"title":"Dev Test","choices":["A","B"]}'
87
+ ```
88
+
89
+ ### 🔍 Testing with MCP Inspector
90
+ 1. **Build Everything**: `npm run build` at the root.
91
+ 2. **Run Inspector** from the project root:
92
+ ```bash
93
+ npx -y @modelcontextprotocol/inspector node dist/index.js
94
+ ```
95
+
96
+ ## 📝 License
97
+ MIT
Binary file
package/dist/index.js ADDED
@@ -0,0 +1,184 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { spawn } from "child_process";
5
+ import path from "path";
6
+ import { fileURLToPath } from "url";
7
+ import { Command } from "commander";
8
+ import fs from "fs";
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ // CLI Arguments for the MCP server itself
12
+ const program = new Command();
13
+ program
14
+ .option("--timeout <number>", "Default timeout in seconds", "60")
15
+ .option("--binary-path <string>", "Path to the native-ui binary")
16
+ .parse(process.argv);
17
+ const options = program.opts();
18
+ const DEFAULT_TIMEOUT = parseInt(options.timeout);
19
+ /**
20
+ * Strategy to find the native-ui binary.
21
+ * 1. Explicit --binary-path flag.
22
+ * 2. Monorepo dev path.
23
+ * 3. npm installation path (bin/native-ui-<platform>-<arch>).
24
+ */
25
+ function getBinaryPath() {
26
+ if (options.binaryPath)
27
+ return options.binaryPath;
28
+ const platform = process.platform;
29
+ const arch = process.arch;
30
+ const exeSuffix = platform === "win32" ? ".exe" : "";
31
+ // 1. Dev Path (Monorepo)
32
+ // When running from root/dist/index.js, native-ui is in root/packages/native-ui/...
33
+ const devPath = path.resolve(__dirname, `../packages/native-ui/src-tauri/target/release/native-ui${exeSuffix}`);
34
+ // 2. NPM Path (standardized binary name)
35
+ const npmPath = path.resolve(__dirname, `../bin/native-ui-${platform}-${arch}${exeSuffix}`);
36
+ if (fs.existsSync(npmPath))
37
+ return npmPath;
38
+ if (fs.existsSync(devPath))
39
+ return devPath;
40
+ // Fallback to debug path if release doesn't exist
41
+ const debugPath = path.resolve(__dirname, `../packages/native-ui/src-tauri/target/debug/native-ui${exeSuffix}`);
42
+ if (fs.existsSync(debugPath))
43
+ return debugPath;
44
+ return npmPath;
45
+ }
46
+ /**
47
+ * Resolves the recommended choice string to its index in the choices array.
48
+ * Returns -1 if no recommendation is provided.
49
+ * @throws McpError if the recommendation doesn't match any choice.
50
+ */
51
+ export function resolveRecommendedIndex(choices, recommended) {
52
+ if (recommended === undefined || recommended === null)
53
+ return -1;
54
+ const target = recommended.trim();
55
+ const index = choices.findIndex(c => c.trim() === target);
56
+ if (index === -1) {
57
+ throw new McpError(ErrorCode.InvalidParams, `recommended choice "${recommended}" does not match any available choices. Available: ${choices.join(", ")}`);
58
+ }
59
+ return index;
60
+ }
61
+ const server = new Server({
62
+ name: "mcp-interactive-choice",
63
+ version: "1.0.0",
64
+ }, {
65
+ capabilities: {
66
+ tools: {},
67
+ },
68
+ });
69
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
70
+ return {
71
+ tools: [
72
+ {
73
+ name: "ask_user",
74
+ description: "Ask the user a question with several choices via a native GUI window. Supports Markdown in the body and a recommended choice.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ title: {
79
+ type: "string",
80
+ description: "(Optional) A concise, high-level summary of the decision required.",
81
+ },
82
+ body: {
83
+ type: "string",
84
+ description: "(Optional) Detailed context or explanation. Supports Markdown (code blocks, lists, etc.) to help the user make an informed choice.",
85
+ },
86
+ choices: {
87
+ type: "array",
88
+ items: { type: "string" },
89
+ description: "(Required) A list of predefined options for the user to select from.",
90
+ },
91
+ recommended: {
92
+ type: "string",
93
+ description: "(Optional) One of the exact strings from the 'choices' array that the agent recommends. The UI will highlight this option.",
94
+ },
95
+ allowCustom: {
96
+ type: "boolean",
97
+ description: "(Optional) Whether to provide a text area for the user to type a custom response not in the choices list. Defaults to false.",
98
+ default: false
99
+ },
100
+ timeoutSec: {
101
+ type: "number",
102
+ description: `(Optional) How long to wait for a user response in seconds. Defaults to ${DEFAULT_TIMEOUT}. If exceeded, the tool returns a timeout error.`,
103
+ }
104
+ },
105
+ required: ["choices"],
106
+ },
107
+ },
108
+ ],
109
+ };
110
+ });
111
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
112
+ if (request.params.name === "ask_user") {
113
+ const args = request.params.arguments;
114
+ const choices = args.choices;
115
+ const timeout = (args.timeoutSec || DEFAULT_TIMEOUT) * 1000;
116
+ const recommendedIndex = resolveRecommendedIndex(choices, args.recommended);
117
+ const inputData = {
118
+ title: args.title || "Action Required",
119
+ body: args.body || "",
120
+ choices,
121
+ recommendedIndex,
122
+ allowCustom: !!args.allowCustom,
123
+ };
124
+ const binaryPath = getBinaryPath();
125
+ return new Promise((resolve, reject) => {
126
+ const child = spawn(binaryPath, ["--input", JSON.stringify(inputData)], {
127
+ stdio: ["ignore", "pipe", "inherit"],
128
+ });
129
+ let stdoutData = "";
130
+ child.stdout.on("data", (data) => {
131
+ stdoutData += data.toString();
132
+ });
133
+ const timer = setTimeout(() => {
134
+ child.kill("SIGKILL");
135
+ resolve({
136
+ content: [{ type: "text", text: "Error: User feedback timed out." }],
137
+ isError: true,
138
+ });
139
+ }, timeout);
140
+ child.on("close", (code) => {
141
+ clearTimeout(timer);
142
+ if (code === 0) {
143
+ try {
144
+ const cleanedStdout = stdoutData.split('\n')
145
+ .filter(line => !line.trim().startsWith('DEBUG'))
146
+ .join('\n')
147
+ .trim();
148
+ const result = JSON.parse(cleanedStdout);
149
+ const textResult = result.custom_input || result.choice || "No selection made";
150
+ resolve({
151
+ content: [{ type: "text", text: textResult }],
152
+ });
153
+ }
154
+ catch (e) {
155
+ resolve({
156
+ content: [{ type: "text", text: `Error parsing result: ${stdoutData}` }],
157
+ isError: true,
158
+ });
159
+ }
160
+ }
161
+ else {
162
+ resolve({
163
+ content: [{ type: "text", text: `Tool window closed unexpectedly (code ${code})` }],
164
+ isError: true,
165
+ });
166
+ }
167
+ });
168
+ child.on("error", (err) => {
169
+ clearTimeout(timer);
170
+ reject(new McpError(ErrorCode.InternalError, `Failed to launch interactive window: ${err.message}`));
171
+ });
172
+ });
173
+ }
174
+ throw new McpError(ErrorCode.MethodNotFound, `Tool not found: ${request.params.name}`);
175
+ });
176
+ async function main() {
177
+ const transport = new StdioServerTransport();
178
+ await server.connect(transport);
179
+ console.error("Interactive Choice MCP Server (Tauri) running on stdio");
180
+ }
181
+ main().catch((error) => {
182
+ console.error("Fatal error in main():", error);
183
+ process.exit(1);
184
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { resolveRecommendedIndex } from "./index.js";
3
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
4
+ describe("resolveRecommendedIndex", () => {
5
+ const choices = ["Apple", "Banana", "Cherry"];
6
+ it("should return -1 if recommended is null or undefined", () => {
7
+ expect(resolveRecommendedIndex(choices, null)).toBe(-1);
8
+ expect(resolveRecommendedIndex(choices, undefined)).toBe(-1);
9
+ });
10
+ it("should return the correct index for a valid recommendation", () => {
11
+ expect(resolveRecommendedIndex(choices, "Banana")).toBe(1);
12
+ });
13
+ it("should handle whitespace correctly", () => {
14
+ expect(resolveRecommendedIndex(choices, " Cherry ")).toBe(2);
15
+ });
16
+ it("should throw McpError for invalid recommendation", () => {
17
+ expect(() => resolveRecommendedIndex(choices, "Dragonfruit")).toThrow(McpError);
18
+ try {
19
+ resolveRecommendedIndex(choices, "Dragonfruit");
20
+ }
21
+ catch (e) {
22
+ expect(e.code).toBe(ErrorCode.InvalidParams);
23
+ }
24
+ });
25
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, it } from 'vitest';
2
+ import { spawn } from 'child_process';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ describe('spawn native-ui', () => {
9
+ it('should be able to find and execute the native-ui binary with --help', async () => {
10
+ const platform = process.platform;
11
+ const exeSuffix = platform === 'win32' ? '.exe' : '';
12
+ // Check release first, then debug
13
+ const releasePath = path.resolve(__dirname, `../packages/native-ui/src-tauri/target/release/native-ui${exeSuffix}`);
14
+ const debugPath = path.resolve(__dirname, `../packages/native-ui/src-tauri/target/debug/native-ui${exeSuffix}`);
15
+ const binaryPath = fs.existsSync(releasePath) ? releasePath : debugPath;
16
+ if (!fs.existsSync(binaryPath)) {
17
+ console.warn(`Binary not found at ${binaryPath}, skipping execution test. Run 'npm run build' first.`);
18
+ return;
19
+ }
20
+ return new Promise((resolve, reject) => {
21
+ const child = spawn(binaryPath, ['--help'], {
22
+ stdio: ['ignore', 'pipe', 'pipe']
23
+ });
24
+ let stdout = '';
25
+ child.stdout.on('data', (data) => {
26
+ stdout += data.toString();
27
+ });
28
+ child.on('close', (code) => {
29
+ // Tauri apps return 0 for help usually, but some return non-zero if no event loop
30
+ // We just care that it lived long enough to print something or exit cleanly
31
+ console.log('Tauri help output:', stdout);
32
+ resolve(true);
33
+ });
34
+ child.on('error', (err) => {
35
+ reject(err);
36
+ });
37
+ });
38
+ }, 10000); // 10s timeout
39
+ });
package/dist/ui ADDED
@@ -0,0 +1,262 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Needs Your Choice</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --primary: #4f46e5;
13
+ --primary-hover: #4338ca;
14
+ --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
15
+ --glass: rgba(255, 255, 255, 0.05);
16
+ --glass-border: rgba(255, 255, 255, 0.1);
17
+ --text-main: #f8fafc;
18
+ --text-dim: #94a3b8;
19
+ }
20
+
21
+ * {
22
+ margin: 0;
23
+ padding: 0;
24
+ box-sizing: border-box;
25
+ font-family: 'Outfit', sans-serif;
26
+ }
27
+
28
+ body {
29
+ min-height: 100vh;
30
+ display: flex;
31
+ align-items: center;
32
+ justify-content: center;
33
+ background: var(--bg-gradient);
34
+ background-attachment: fixed;
35
+ color: var(--text-main);
36
+ overflow: hidden;
37
+ }
38
+
39
+ /* Abstract blobs for aesthetics */
40
+ .blob {
41
+ position: absolute;
42
+ width: 500px;
43
+ height: 500px;
44
+ background: linear-gradient(180deg, rgba(79, 70, 229, 0.2) 0%, rgba(147, 51, 234, 0.1) 100%);
45
+ filter: blur(80px);
46
+ border-radius: 50%;
47
+ z-index: -1;
48
+ animation: move 20s infinite alternate;
49
+ }
50
+
51
+ .blob-1 { top: -100px; left: -100px; }
52
+ .blob-2 { bottom: -100px; right: -100px; animation-delay: -5s; }
53
+
54
+ @keyframes move {
55
+ from { transform: translate(0, 0) scale(1); }
56
+ to { transform: translate(50px, 50px) scale(1.1); }
57
+ }
58
+
59
+ .container {
60
+ width: 100%;
61
+ max-width: 500px;
62
+ padding: 2rem;
63
+ background: var(--glass);
64
+ backdrop-filter: blur(12px);
65
+ border: 1px solid var(--glass-border);
66
+ border-radius: 24px;
67
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
68
+ animation: fadeIn 0.6s cubic-bezier(0.16, 1, 0.3, 1);
69
+ }
70
+
71
+ @keyframes fadeIn {
72
+ from { opacity: 0; transform: translateY(20px); }
73
+ to { opacity: 1; transform: translateY(0); }
74
+ }
75
+
76
+ .header {
77
+ margin-bottom: 2rem;
78
+ text-align: center;
79
+ }
80
+
81
+ .ai-indicator {
82
+ display: inline-flex;
83
+ align-items: center;
84
+ gap: 0.5rem;
85
+ background: rgba(79, 70, 229, 0.1);
86
+ padding: 0.5rem 1rem;
87
+ border-radius: 100px;
88
+ color: #818cf8;
89
+ font-size: 0.875rem;
90
+ font-weight: 600;
91
+ margin-bottom: 1rem;
92
+ border: 1px solid rgba(79, 70, 229, 0.2);
93
+ }
94
+
95
+ .pulse {
96
+ width: 8px;
97
+ height: 8px;
98
+ background: #818cf8;
99
+ border-radius: 50%;
100
+ animation: pulse 2s infinite;
101
+ }
102
+
103
+ @keyframes pulse {
104
+ 0% { transform: scale(1); opacity: 1; box-shadow: 0 0 0 0 rgba(129, 140, 248, 0.7); }
105
+ 70% { transform: scale(1); opacity: 1; box-shadow: 0 0 0 10px rgba(129, 140, 248, 0); }
106
+ 100% { transform: scale(1); opacity: 0; }
107
+ }
108
+
109
+ h1 {
110
+ font-size: 1.5rem;
111
+ font-weight: 600;
112
+ line-height: 1.4;
113
+ color: var(--text-main);
114
+ }
115
+
116
+ .choices {
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 0.75rem;
120
+ }
121
+
122
+ button {
123
+ width: 100%;
124
+ padding: 1.25rem;
125
+ background: rgba(255, 255, 255, 0.03);
126
+ border: 1px solid var(--glass-border);
127
+ border-radius: 16px;
128
+ color: var(--text-main);
129
+ font-size: 1rem;
130
+ font-weight: 400;
131
+ text-align: left;
132
+ cursor: pointer;
133
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ }
138
+
139
+ button:hover {
140
+ background: rgba(255, 255, 255, 0.08);
141
+ border-color: rgba(255, 255, 255, 0.2);
142
+ transform: translateX(4px);
143
+ }
144
+
145
+ button:active {
146
+ transform: scale(0.98);
147
+ }
148
+
149
+ button .arrow {
150
+ opacity: 0;
151
+ transition: 0.2s;
152
+ transform: translateX(-10px);
153
+ }
154
+
155
+ button:hover .arrow {
156
+ opacity: 1;
157
+ transform: translateX(0);
158
+ }
159
+
160
+ .footer {
161
+ margin-top: 2rem;
162
+ text-align: center;
163
+ font-size: 0.75rem;
164
+ color: var(--text-dim);
165
+ }
166
+
167
+ .loading-overlay {
168
+ position: fixed;
169
+ inset: 0;
170
+ background: var(--bg-gradient);
171
+ display: flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ z-index: 100;
175
+ transition: opacity 0.5s ease;
176
+ }
177
+
178
+ .loading-overlay.hidden {
179
+ opacity: 0;
180
+ pointer-events: none;
181
+ }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div class="blob blob-1"></div>
186
+ <div class="blob blob-2"></div>
187
+
188
+ <div id="loading" class="loading-overlay">
189
+ <div class="ai-indicator">
190
+ <div class="pulse"></div>
191
+ Connecting...
192
+ </div>
193
+ </div>
194
+
195
+ <div class="container">
196
+ <div class="header">
197
+ <div class="ai-indicator">
198
+ <div class="pulse"></div>
199
+ Agent Interaction
200
+ </div>
201
+ <h1 id="question">Please wait while the question loads...</h1>
202
+ </div>
203
+
204
+ <div id="choices" class="choices">
205
+ <!-- Buttons will be injected here -->
206
+ </div>
207
+
208
+ <div class="footer">
209
+ Choice will be returned to the AI assistant immediately.
210
+ </div>
211
+ </div>
212
+
213
+ <script>
214
+ async function init() {
215
+ try {
216
+ const response = await fetch('/api/details');
217
+ const data = await response.json();
218
+
219
+ document.getElementById('question').textContent = data.question;
220
+ const choicesContainer = document.getElementById('choices');
221
+
222
+ data.choices.forEach(choice => {
223
+ const btn = document.createElement('button');
224
+ btn.innerHTML = `${choice} <span class="arrow">→</span>`;
225
+ btn.onclick = () => submit(choice);
226
+ choicesContainer.appendChild(btn);
227
+ });
228
+
229
+ document.getElementById('loading').classList.add('hidden');
230
+ } catch (err) {
231
+ console.error('Failed to load details', err);
232
+ document.getElementById('question').textContent = "Error: Could not connect to MCP server.";
233
+ }
234
+ }
235
+
236
+ async function submit(choice) {
237
+ try {
238
+ await fetch('/api/respond', {
239
+ method: 'POST',
240
+ headers: { 'Content-Type': 'application/json' },
241
+ body: JSON.stringify({ choice })
242
+ });
243
+ // Close window if possible
244
+ window.close();
245
+ // If window.close() is blocked (standard browser behavior), show success
246
+ document.querySelector('.container').innerHTML = `
247
+ <div style="text-align: center; padding: 2rem;">
248
+ <div class="ai-indicator" style="background: rgba(16, 185, 129, 0.1); color: #10b981; border-color: rgba(16, 185, 129, 0.2);">✓ Response Sent</div>
249
+ <h1 style="margin-top: 1rem;">Done!</h1>
250
+ <p style="color: var(--text-dim); margin-top: 0.5rem;">You can close this tab now.</p>
251
+ </div>
252
+ `;
253
+ } catch (err) {
254
+ console.error('Failed to submit response', err);
255
+ }
256
+ }
257
+
258
+ init();
259
+ </script>
260
+ </body>
261
+ </html>
262
+ 
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "mcp-interactive-choice",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "MCP server for interactive user questions with native Tauri UI",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "mcp-interactive-choice": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "bin",
13
+ "README.md"
14
+ ],
15
+ "workspaces": [
16
+ "packages/*"
17
+ ],
18
+ "scripts": {
19
+ "build:server": "tsc",
20
+ "build:ui": "npm run build --workspace=native-ui",
21
+ "build": "npm run build:ui && npm run build:server && node scripts/copy-binaries.js",
22
+ "start": "node dist/index.js",
23
+ "test": "vitest run"
24
+ },
25
+ "dependencies": {
26
+ "@modelcontextprotocol/sdk": "^1.0.3",
27
+ "commander": "^11.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/node": "^20.11.0",
31
+ "typescript": "^5.3.3",
32
+ "vitest": "^4.0.18"
33
+ }
34
+ }