npm-run-mcp-server 0.2.9 → 0.2.11
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 +93 -83
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +400 -57
- package/npm-run-mcp.config.schema.json +46 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -18,19 +18,28 @@
|
|
|
18
18
|
|
|
19
19
|
- [Install](#install)
|
|
20
20
|
- [Usage](#usage)
|
|
21
|
+
- [As an MCP Server](#as-an-mcp-server)
|
|
22
|
+
- [As a CLI Tool](#as-a-cli-tool)
|
|
21
23
|
- [Configuration](#configuration)
|
|
22
|
-
- [GitHub Copilot
|
|
23
|
-
- [Claude Code (VS Code extension)](#claude-code-vs-code-extension)
|
|
24
|
-
- [Claude Code (terminal / standalone)](#claude-code-terminal--standalone)
|
|
24
|
+
- [GitHub Copilot (VS Code)](#github-copilot-vs-code)
|
|
25
25
|
- [Cursor](#cursor)
|
|
26
|
+
- [Claude Code](#claude-code)
|
|
27
|
+
- [Multi-Project Workflow](#multi-project-workflow)
|
|
28
|
+
- [Auto-Restart on Script Changes](#auto-restart-on-script-changes)
|
|
29
|
+
- [Script Exposure Config](#script-exposure-config)
|
|
26
30
|
- [Install from source](#install-from-source)
|
|
27
31
|
- [Testing with MCP Inspector](#testing-with-mcp-inspector)
|
|
28
32
|
- [CLI Options](#cli-options)
|
|
29
33
|
- [Contributing](#contributing)
|
|
34
|
+
- [Reporting Issues](#reporting-issues)
|
|
35
|
+
- [Submitting Changes](#submitting-changes)
|
|
36
|
+
- [Development Setup](#development-setup)
|
|
30
37
|
- [License](#license)
|
|
31
38
|
|
|
32
39
|
## Install
|
|
33
40
|
|
|
41
|
+
Installation options.
|
|
42
|
+
|
|
34
43
|
```bash
|
|
35
44
|
npm i -D npm-run-mcp-server
|
|
36
45
|
# or globally
|
|
@@ -41,17 +50,21 @@ npx npm-run-mcp-server
|
|
|
41
50
|
|
|
42
51
|
## Usage
|
|
43
52
|
|
|
53
|
+
MCP server and CLI tool usage.
|
|
54
|
+
|
|
44
55
|
### As an MCP Server
|
|
45
56
|
|
|
46
57
|
Add this server to your MCP host configuration. It uses stdio and automatically detects your project's `package.json` using workspace environment variables or by walking up from the current working directory.
|
|
47
58
|
|
|
48
59
|
**Key Features:**
|
|
49
60
|
- **Automatic Workspace Detection**: Works seamlessly across different projects without configuration changes
|
|
50
|
-
- **Smart Tool Names**: Script names with colons (like `
|
|
61
|
+
- **Smart Tool Names**: Script names with colons (like `test:unit`) are automatically converted to valid tool names (`test_unit`)
|
|
51
62
|
- **Rich Descriptions**: Each tool includes the actual script command in its description
|
|
52
63
|
- **Package Manager Detection**: Automatically detects npm, pnpm, yarn, or bun
|
|
53
|
-
- **Optional Arguments**: Each tool accepts
|
|
54
|
-
- **Auto-Restart on Changes**: Automatically restarts when `package.json`
|
|
64
|
+
- **Optional Arguments**: Each tool accepts optional `args` (`string` or `string[]`) appended after `--` when running the script
|
|
65
|
+
- **Auto-Restart on Changes**: Automatically restarts when `package.json` or config changes, ensuring tools are always up-to-date
|
|
66
|
+
|
|
67
|
+
Note: scripts run inside the target project. If they rely on local dependencies (eslint, vitest, tsc), install them first (for example, `npm install`).
|
|
55
68
|
|
|
56
69
|
### As a CLI Tool
|
|
57
70
|
|
|
@@ -69,12 +82,23 @@ npx npm-run-mcp-server --cwd /path/to/project --list-scripts
|
|
|
69
82
|
|
|
70
83
|
# Override package manager detection
|
|
71
84
|
npx npm-run-mcp-server --pm yarn --list-scripts
|
|
85
|
+
|
|
86
|
+
# Use an explicit config file (relative to the project directory, or absolute)
|
|
87
|
+
npx npm-run-mcp-server --cwd /path/to/project --config npm-run-mcp.config.json --verbose
|
|
72
88
|
```
|
|
73
89
|
|
|
74
90
|
## Configuration
|
|
75
91
|
|
|
76
|
-
|
|
92
|
+
Setup instructions for AI agents.
|
|
93
|
+
|
|
94
|
+
### GitHub Copilot (VS Code)
|
|
95
|
+
|
|
96
|
+
#### Via UI
|
|
97
|
+
1. Open VS Code settings
|
|
98
|
+
2. Search for "MCP"
|
|
99
|
+
3. Add server configuration in settings.json
|
|
77
100
|
|
|
101
|
+
#### Via Config File
|
|
78
102
|
Option A — per-workspace via `.vscode/mcp.json` (recommended for multi-project use):
|
|
79
103
|
|
|
80
104
|
```json
|
|
@@ -101,39 +125,23 @@ Option B — user settings (`settings.json`):
|
|
|
101
125
|
}
|
|
102
126
|
```
|
|
103
127
|
|
|
104
|
-
**Note**: The server automatically detects the current project's `package.json` using workspace environment variables (like `WORKSPACE_FOLDER_PATHS`) or by walking up from the current working directory. No hardcoded paths are needed - it works seamlessly across all your projects.
|
|
105
|
-
|
|
106
128
|
Then open Copilot Chat, switch to Agent mode, and start the `npm-scripts` server from the tools panel.
|
|
107
129
|
|
|
108
|
-
###
|
|
109
|
-
|
|
110
|
-
The MCP server is designed to work seamlessly across multiple projects without configuration changes:
|
|
111
|
-
|
|
112
|
-
- **VS Code/Cursor**: The server automatically detects the current workspace using environment variables like `WORKSPACE_FOLDER_PATHS`
|
|
113
|
-
- **Claude Desktop**: The server uses the working directory where Claude is launched
|
|
114
|
-
- **No Hardcoded Paths**: All examples use `npx npm-run-mcp-server` without `--cwd` flags
|
|
115
|
-
- **Smart Detection**: The server first tries workspace environment variables, then falls back to walking up the directory tree to find the nearest `package.json`
|
|
116
|
-
- **Cross-Platform**: Handles Windows/WSL path conversions automatically
|
|
117
|
-
|
|
118
|
-
This means you can use the same MCP configuration across all your projects, and the server will automatically target the correct project based on your current workspace.
|
|
119
|
-
|
|
120
|
-
### Auto-Restart on Script Changes
|
|
121
|
-
|
|
122
|
-
The MCP server automatically monitors your `package.json` file for changes. When you add, remove, or modify scripts, the server will:
|
|
123
|
-
|
|
124
|
-
1. **Detect the change** and log it (with `--verbose` flag)
|
|
125
|
-
2. **Gracefully exit** to allow the MCP client to restart the server
|
|
126
|
-
3. **Reload with new tools** based on the updated scripts
|
|
130
|
+
### Cursor
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
#### Via UI
|
|
133
|
+
1. Open Settings -> MCP Servers -> Add MCP Server
|
|
134
|
+
2. Type: NPX Package
|
|
135
|
+
3. Command: `npx`
|
|
136
|
+
4. Arguments: `-y npm-run-mcp-server`
|
|
137
|
+
5. Save and start the server from the tools list
|
|
129
138
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Add to VS Code user/workspace settings (`settings.json`):
|
|
139
|
+
#### Via Config File
|
|
140
|
+
Add to Cursor's MCP configuration:
|
|
133
141
|
|
|
134
142
|
```json
|
|
135
143
|
{
|
|
136
|
-
"
|
|
144
|
+
"servers": {
|
|
137
145
|
"npm-scripts": {
|
|
138
146
|
"command": "npx",
|
|
139
147
|
"args": ["-y", "npm-run-mcp-server"]
|
|
@@ -142,20 +150,19 @@ Add to VS Code user/workspace settings (`settings.json`):
|
|
|
142
150
|
}
|
|
143
151
|
```
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Restart the extension and confirm the server/tools appear.
|
|
153
|
+
### Claude Code
|
|
148
154
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
155
|
+
#### Via Terminal
|
|
156
|
+
```bash
|
|
157
|
+
claude mcp add npm-scripts npx -y npm-run-mcp-server
|
|
158
|
+
```
|
|
152
159
|
|
|
160
|
+
#### Via Config File
|
|
161
|
+
Add to Claude Code's config file:
|
|
153
162
|
- Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
154
163
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
155
164
|
- Linux: `~/.config/Claude/claude_desktop_config.json`
|
|
156
165
|
|
|
157
|
-
**Recommended approach** - Using npx (works across all projects):
|
|
158
|
-
|
|
159
166
|
```json
|
|
160
167
|
{
|
|
161
168
|
"mcpServers": {
|
|
@@ -167,48 +174,58 @@ Add this server to Claude's global config file (paths vary by OS). Create the fi
|
|
|
167
174
|
}
|
|
168
175
|
```
|
|
169
176
|
|
|
170
|
-
|
|
177
|
+
Restart Claude Code after editing the config.
|
|
171
178
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"args": ["/absolute/path/to/npm-run-mcp-server/dist/index.js"]
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
```
|
|
179
|
+
### Multi-Project Workflow
|
|
180
|
+
|
|
181
|
+
The MCP server automatically detects your project's `package.json` using workspace environment variables or by walking up from the current working directory. No hardcoded paths needed - it works seamlessly across all your projects.
|
|
182
|
+
|
|
183
|
+
### Auto-Restart on Script Changes
|
|
182
184
|
|
|
183
|
-
|
|
185
|
+
The server automatically monitors your `package.json` file (and `npm-run-mcp.config.json` if present) for changes. When you modify scripts or config, the server gracefully exits to allow the MCP client to restart with updated tools.
|
|
184
186
|
|
|
185
|
-
|
|
187
|
+
### Script Exposure Config
|
|
188
|
+
|
|
189
|
+
You can make the tool surface more deterministic by explicitly choosing which scripts are exposed and by defining per-script tool metadata.
|
|
190
|
+
|
|
191
|
+
Create `npm-run-mcp.config.json` (or `.npm-run-mcp.json`) next to your project's `package.json`:
|
|
186
192
|
|
|
187
193
|
```json
|
|
188
194
|
{
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
"
|
|
194
|
-
|
|
195
|
+
"include": ["build", "lint", "test:unit"],
|
|
196
|
+
"exclude": ["dev", "test:e2e"],
|
|
197
|
+
"scripts": {
|
|
198
|
+
"test:unit": {
|
|
199
|
+
"toolName": "test_unit",
|
|
200
|
+
"description": "Run unit tests",
|
|
201
|
+
"inputSchema": {
|
|
202
|
+
"type": "object",
|
|
203
|
+
"properties": {
|
|
204
|
+
"watch": { "type": "boolean" },
|
|
205
|
+
"run": { "type": "boolean" }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
"lint": {
|
|
210
|
+
"description": "Lint the codebase",
|
|
211
|
+
"inputSchema": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": {
|
|
214
|
+
"fix": { "type": "boolean" }
|
|
215
|
+
}
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
```
|
|
200
221
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
-
|
|
206
|
-
-
|
|
207
|
-
-
|
|
208
|
-
- Arguments: `-y npm-run-mcp-server`
|
|
209
|
-
- Save and start the server from the tools list
|
|
210
|
-
|
|
211
|
-
**Note**: This configuration automatically works across all your projects. The server will target the current project's `package.json` wherever Cursor is opened.
|
|
222
|
+
Notes:
|
|
223
|
+
- `include` and `exclude` are exact script names.
|
|
224
|
+
- `toolName` lets you resolve naming collisions after sanitization.
|
|
225
|
+
- `inputSchema` extends the default input model (and `args` is always available).
|
|
226
|
+
- Tool input fields (other than `args`) are converted to CLI flags, e.g. `{ "watch": true }` becomes `--watch` and `{ "port": 3000 }` becomes `--port 3000`.
|
|
227
|
+
- If filters result in zero tools, the server logs a warning so misconfigurations are easy to spot.
|
|
228
|
+
- Config files support JSONC (comments + trailing commas). A JSON Schema is published as `npm-run-mcp.config.schema.json`.
|
|
212
229
|
|
|
213
230
|
### Install from source (for testing in another project)
|
|
214
231
|
|
|
@@ -255,7 +272,7 @@ Optional CLI flags you can pass in `args`:
|
|
|
255
272
|
|
|
256
273
|
## Testing with MCP Inspector
|
|
257
274
|
|
|
258
|
-
Test the server locally
|
|
275
|
+
Test the server locally.
|
|
259
276
|
|
|
260
277
|
```bash
|
|
261
278
|
# Start MCP Inspector
|
|
@@ -272,16 +289,17 @@ You should see your package.json scripts listed as available tools. Try running
|
|
|
272
289
|
|
|
273
290
|
## CLI Options
|
|
274
291
|
|
|
275
|
-
|
|
292
|
+
Command-line flags.
|
|
276
293
|
|
|
277
294
|
- `--cwd <path>` - Specify working directory (defaults to current directory)
|
|
295
|
+
- `--config <path>` - Use an explicit config file path (relative to the project directory, or absolute)
|
|
278
296
|
- `--pm <manager>` - Override package manager detection (npm|pnpm|yarn|bun)
|
|
279
297
|
- `--verbose` - Enable detailed logging to stderr
|
|
280
298
|
- `--list-scripts` - List available scripts and exit
|
|
281
299
|
|
|
282
300
|
## Contributing
|
|
283
301
|
|
|
284
|
-
|
|
302
|
+
Contributions welcome! How to help with development, reporting issues, and submitting changes.
|
|
285
303
|
|
|
286
304
|
### Reporting Issues
|
|
287
305
|
|
|
@@ -311,15 +329,7 @@ npm run test
|
|
|
311
329
|
|
|
312
330
|
The project uses a custom build script located in `scripts/build.cjs` that handles TypeScript compilation and shebang injection for the executable.
|
|
313
331
|
|
|
314
|
-
### Guidelines
|
|
315
|
-
|
|
316
|
-
- Follow the existing code style
|
|
317
|
-
- Add tests for new features
|
|
318
|
-
- Update documentation as needed
|
|
319
|
-
- Keep commits focused and descriptive
|
|
320
332
|
|
|
321
333
|
## License
|
|
322
334
|
|
|
323
|
-
MIT
|
|
324
|
-
|
|
325
|
-
|
|
335
|
+
MIT License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
CHANGED
|
@@ -3,11 +3,192 @@ import { readFileSync, existsSync, watch } from 'fs';
|
|
|
3
3
|
import { promises as fsp } from 'fs';
|
|
4
4
|
import { dirname, resolve } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
6
|
+
import spawn from 'cross-spawn';
|
|
7
|
+
import { parse as parseJsonc } from 'jsonc-parser';
|
|
8
8
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
9
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
const MCP_CONFIG_FILES = ['npm-run-mcp.config.json', '.npm-run-mcp.json'];
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function parseMcpConfig(raw) {
|
|
16
|
+
if (!isPlainObject(raw))
|
|
17
|
+
return {};
|
|
18
|
+
const include = Array.isArray(raw.include) ? raw.include.filter((v) => typeof v === 'string') : undefined;
|
|
19
|
+
const exclude = Array.isArray(raw.exclude) ? raw.exclude.filter((v) => typeof v === 'string') : undefined;
|
|
20
|
+
let scripts;
|
|
21
|
+
if (isPlainObject(raw.scripts)) {
|
|
22
|
+
scripts = {};
|
|
23
|
+
for (const [name, value] of Object.entries(raw.scripts)) {
|
|
24
|
+
if (!isPlainObject(value))
|
|
25
|
+
continue;
|
|
26
|
+
scripts[name] = {
|
|
27
|
+
toolName: typeof value.toolName === 'string' ? value.toolName : undefined,
|
|
28
|
+
description: typeof value.description === 'string' ? value.description : undefined,
|
|
29
|
+
inputSchema: isPlainObject(value.inputSchema) ? value.inputSchema : undefined,
|
|
30
|
+
argsDescription: typeof value.argsDescription === 'string' ? value.argsDescription : undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return { include, exclude, scripts };
|
|
35
|
+
}
|
|
36
|
+
function parseJsonOrJsonc(text, filePathForErrors) {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const errors = [];
|
|
42
|
+
const parsed = parseJsonc(text, errors, { allowTrailingComma: true, disallowComments: false });
|
|
43
|
+
if (errors.length > 0) {
|
|
44
|
+
const formatted = errors
|
|
45
|
+
.slice(0, 3)
|
|
46
|
+
.map((e) => `code=${e.error} offset=${e.offset} length=${e.length}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
throw new Error(`Invalid JSON/JSONC in ${filePathForErrors} (${formatted})`);
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readProjectMcpConfig(projectDir, verbose, configArg) {
|
|
54
|
+
if (typeof configArg === 'string' && configArg.length > 0) {
|
|
55
|
+
const candidate = resolve(projectDir, configArg);
|
|
56
|
+
if (!existsSync(candidate)) {
|
|
57
|
+
console.error(`npm-run-mcp-server: Config file not found: ${candidate}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const raw = await fsp.readFile(candidate, 'utf8');
|
|
62
|
+
const parsed = parseJsonOrJsonc(raw, candidate);
|
|
63
|
+
const config = parseMcpConfig(parsed);
|
|
64
|
+
if (verbose) {
|
|
65
|
+
console.error(`[mcp] using config file: ${candidate}`);
|
|
66
|
+
}
|
|
67
|
+
return { config, configPath: candidate };
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error?.message ? String(error.message) : String(error);
|
|
71
|
+
console.error(`npm-run-mcp-server: Failed to read config file ${candidate}: ${message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const filename of MCP_CONFIG_FILES) {
|
|
76
|
+
const candidate = resolve(projectDir, filename);
|
|
77
|
+
if (!existsSync(candidate))
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
const raw = await fsp.readFile(candidate, 'utf8');
|
|
81
|
+
const parsed = parseJsonOrJsonc(raw, candidate);
|
|
82
|
+
const config = parseMcpConfig(parsed);
|
|
83
|
+
if (verbose) {
|
|
84
|
+
console.error(`[mcp] using config file: ${candidate}`);
|
|
85
|
+
}
|
|
86
|
+
return { config, configPath: candidate };
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const message = error?.message ? String(error.message) : String(error);
|
|
90
|
+
console.error(`npm-run-mcp-server: Failed to read config file ${candidate}: ${message}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { config: {}, configPath: null };
|
|
95
|
+
}
|
|
96
|
+
function normalizeToolName(name) {
|
|
97
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9_-]/g, '_');
|
|
98
|
+
return normalized.length > 0 ? normalized : 'script';
|
|
99
|
+
}
|
|
100
|
+
function jsonSchemaToZod(schema) {
|
|
101
|
+
if (!isPlainObject(schema))
|
|
102
|
+
return z.any();
|
|
103
|
+
if (Array.isArray(schema.enum) && schema.enum.every((v) => typeof v === 'string')) {
|
|
104
|
+
const values = schema.enum;
|
|
105
|
+
const [first, ...rest] = values;
|
|
106
|
+
let base = first ? z.literal(first) : z.string();
|
|
107
|
+
for (const v of rest)
|
|
108
|
+
base = z.union([base, z.literal(v)]);
|
|
109
|
+
return typeof schema.description === 'string' ? base.describe(schema.description) : base;
|
|
110
|
+
}
|
|
111
|
+
const type = schema.type;
|
|
112
|
+
let zod;
|
|
113
|
+
switch (type) {
|
|
114
|
+
case 'string':
|
|
115
|
+
zod = z.string();
|
|
116
|
+
break;
|
|
117
|
+
case 'boolean':
|
|
118
|
+
zod = z.boolean();
|
|
119
|
+
break;
|
|
120
|
+
case 'number':
|
|
121
|
+
zod = z.number();
|
|
122
|
+
break;
|
|
123
|
+
case 'integer':
|
|
124
|
+
zod = z.number().int();
|
|
125
|
+
break;
|
|
126
|
+
case 'array': {
|
|
127
|
+
const items = schema.items;
|
|
128
|
+
if (isPlainObject(items) && items.type === 'string')
|
|
129
|
+
zod = z.array(z.string());
|
|
130
|
+
else
|
|
131
|
+
zod = z.array(z.any());
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'object': {
|
|
135
|
+
const properties = isPlainObject(schema.properties) ? schema.properties : {};
|
|
136
|
+
const requiredSet = new Set(Array.isArray(schema.required) ? schema.required.filter((v) => typeof v === 'string') : []);
|
|
137
|
+
const shape = {};
|
|
138
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
139
|
+
let prop = jsonSchemaToZod(value);
|
|
140
|
+
if (!requiredSet.has(key))
|
|
141
|
+
prop = prop.optional();
|
|
142
|
+
shape[key] = prop;
|
|
143
|
+
}
|
|
144
|
+
const obj = z.object(shape);
|
|
145
|
+
zod = schema.additionalProperties === false ? obj.strict() : obj.passthrough();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
default:
|
|
149
|
+
zod = z.any();
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
if (typeof schema.description === 'string')
|
|
153
|
+
zod = zod.describe(schema.description);
|
|
154
|
+
return zod;
|
|
155
|
+
}
|
|
156
|
+
function buildToolInputSchema(configForScript) {
|
|
157
|
+
const base = z
|
|
158
|
+
.object({
|
|
159
|
+
_: z.array(z.string()).optional(),
|
|
160
|
+
args: z
|
|
161
|
+
.union([z.string(), z.array(z.string())])
|
|
162
|
+
.optional()
|
|
163
|
+
.describe(configForScript?.argsDescription ?? 'Optional arguments appended after -- to the script'),
|
|
164
|
+
})
|
|
165
|
+
.passthrough();
|
|
166
|
+
if (!configForScript?.inputSchema)
|
|
167
|
+
return base;
|
|
168
|
+
const converted = jsonSchemaToZod(configForScript.inputSchema);
|
|
169
|
+
if (converted instanceof z.ZodObject) {
|
|
170
|
+
const merged = converted.extend({
|
|
171
|
+
_: base.shape._,
|
|
172
|
+
args: base.shape.args,
|
|
173
|
+
});
|
|
174
|
+
return configForScript.inputSchema && isPlainObject(configForScript.inputSchema) && configForScript.inputSchema.additionalProperties === false
|
|
175
|
+
? merged.strict()
|
|
176
|
+
: merged.passthrough();
|
|
177
|
+
}
|
|
178
|
+
return base;
|
|
179
|
+
}
|
|
180
|
+
function filterScriptNames(scriptNames, config) {
|
|
181
|
+
const include = config.include && config.include.length > 0 ? new Set(config.include) : null;
|
|
182
|
+
const exclude = config.exclude && config.exclude.length > 0 ? new Set(config.exclude) : null;
|
|
183
|
+
const filtered = scriptNames.filter((name) => {
|
|
184
|
+
if (include && !include.has(name))
|
|
185
|
+
return false;
|
|
186
|
+
if (exclude && exclude.has(name))
|
|
187
|
+
return false;
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
190
|
+
return filtered.sort();
|
|
191
|
+
}
|
|
11
192
|
function parseCliArgs(argv) {
|
|
12
193
|
const args = {};
|
|
13
194
|
for (let i = 2; i < argv.length; i += 1) {
|
|
@@ -64,24 +245,150 @@ function detectPackageManager(projectDir, pkg, override) {
|
|
|
64
245
|
return 'npm';
|
|
65
246
|
}
|
|
66
247
|
function buildRunCommand(pm, scriptName, extraArgs) {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
248
|
+
const command = pm;
|
|
249
|
+
const baseArgs = ['run', scriptName];
|
|
250
|
+
const args = extraArgs.length > 0 ? [...baseArgs, '--', ...extraArgs] : baseArgs;
|
|
251
|
+
return { command, args };
|
|
252
|
+
}
|
|
253
|
+
function parseArgString(input) {
|
|
254
|
+
const result = [];
|
|
255
|
+
let current = '';
|
|
256
|
+
let inSingle = false;
|
|
257
|
+
let inDouble = false;
|
|
258
|
+
let escaping = false;
|
|
259
|
+
let tokenActive = false;
|
|
260
|
+
const pushCurrent = () => {
|
|
261
|
+
if (!tokenActive)
|
|
262
|
+
return;
|
|
263
|
+
result.push(current);
|
|
264
|
+
current = '';
|
|
265
|
+
tokenActive = false;
|
|
266
|
+
};
|
|
267
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
268
|
+
const ch = input[i];
|
|
269
|
+
if (escaping) {
|
|
270
|
+
current += ch;
|
|
271
|
+
escaping = false;
|
|
272
|
+
tokenActive = true;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (!inSingle && ch === '\\') {
|
|
276
|
+
escaping = true;
|
|
277
|
+
tokenActive = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (!inDouble && ch === "'" && !escaping) {
|
|
281
|
+
inSingle = !inSingle;
|
|
282
|
+
tokenActive = true;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (!inSingle && ch === '"' && !escaping) {
|
|
286
|
+
inDouble = !inDouble;
|
|
287
|
+
tokenActive = true;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!inSingle && !inDouble && /\s/.test(ch)) {
|
|
291
|
+
pushCurrent();
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
current += ch;
|
|
295
|
+
tokenActive = true;
|
|
79
296
|
}
|
|
297
|
+
if (escaping) {
|
|
298
|
+
current += '\\';
|
|
299
|
+
tokenActive = true;
|
|
300
|
+
}
|
|
301
|
+
pushCurrent();
|
|
302
|
+
return result;
|
|
80
303
|
}
|
|
81
|
-
function
|
|
82
|
-
if (
|
|
304
|
+
function toolInputToExtraArgs(input) {
|
|
305
|
+
if (!isPlainObject(input))
|
|
306
|
+
return [];
|
|
307
|
+
const rawArgsValue = input.args;
|
|
308
|
+
let rawArgs = [];
|
|
309
|
+
if (typeof rawArgsValue === 'string') {
|
|
310
|
+
rawArgs = parseArgString(rawArgsValue);
|
|
311
|
+
}
|
|
312
|
+
else if (Array.isArray(rawArgsValue)) {
|
|
313
|
+
rawArgs = rawArgsValue.map((v) => String(v));
|
|
314
|
+
}
|
|
315
|
+
const positionalValue = input._;
|
|
316
|
+
const positional = Array.isArray(positionalValue) ? positionalValue.map((v) => String(v)) : [];
|
|
317
|
+
const keys = Object.keys(input)
|
|
318
|
+
.filter((k) => k !== 'args' && k !== '_' && input[k] !== undefined)
|
|
319
|
+
.sort();
|
|
320
|
+
const flags = [];
|
|
321
|
+
for (const key of keys) {
|
|
322
|
+
const value = input[key];
|
|
323
|
+
const flag = key.startsWith('-') ? key : `--${key}`;
|
|
324
|
+
if (value === null || value === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
if (typeof value === 'boolean') {
|
|
327
|
+
if (value)
|
|
328
|
+
flags.push(flag);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(value)) {
|
|
332
|
+
for (const item of value) {
|
|
333
|
+
if (item === null || item === undefined)
|
|
334
|
+
continue;
|
|
335
|
+
if (typeof item === 'boolean') {
|
|
336
|
+
if (item)
|
|
337
|
+
flags.push(flag);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
flags.push(flag, String(item));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (typeof value === 'object') {
|
|
346
|
+
flags.push(flag, JSON.stringify(value));
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
flags.push(flag, String(value));
|
|
350
|
+
}
|
|
351
|
+
return [...flags, ...positional, ...rawArgs];
|
|
352
|
+
}
|
|
353
|
+
function trimOutput(out, limit = 12000, totalLength) {
|
|
354
|
+
const total = typeof totalLength === 'number' ? totalLength : out.length;
|
|
355
|
+
if (total <= limit)
|
|
83
356
|
return { text: out, truncated: false };
|
|
84
|
-
return { text: out.slice(0, limit) + `\n...[truncated ${
|
|
357
|
+
return { text: out.slice(0, limit) + `\n...[truncated ${total - limit} chars]`, truncated: true };
|
|
358
|
+
}
|
|
359
|
+
async function runProcess(command, args, options) {
|
|
360
|
+
const outputCaptureLimit = 120000;
|
|
361
|
+
let stdout = '';
|
|
362
|
+
let stderr = '';
|
|
363
|
+
let stdoutTotal = 0;
|
|
364
|
+
let stderrTotal = 0;
|
|
365
|
+
const child = spawn(command, args, {
|
|
366
|
+
cwd: options.cwd,
|
|
367
|
+
env: options.env,
|
|
368
|
+
windowsHide: true,
|
|
369
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
370
|
+
});
|
|
371
|
+
const capture = (kind, chunk) => {
|
|
372
|
+
const text = chunk.toString('utf8');
|
|
373
|
+
if (kind === 'stdout') {
|
|
374
|
+
stdoutTotal += text.length;
|
|
375
|
+
if (stdout.length < outputCaptureLimit)
|
|
376
|
+
stdout += text.slice(0, outputCaptureLimit - stdout.length);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
stderrTotal += text.length;
|
|
380
|
+
if (stderr.length < outputCaptureLimit)
|
|
381
|
+
stderr += text.slice(0, outputCaptureLimit - stderr.length);
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
child.stdout?.on('data', (chunk) => capture('stdout', chunk));
|
|
385
|
+
child.stderr?.on('data', (chunk) => capture('stderr', chunk));
|
|
386
|
+
const exit = await new Promise((resolvePromise, rejectPromise) => {
|
|
387
|
+
child.on('error', (err) => rejectPromise(err));
|
|
388
|
+
child.on('close', (code, signal) => resolvePromise({ exitCode: code, signal }));
|
|
389
|
+
});
|
|
390
|
+
const totalLength = stdoutTotal + stderrTotal + (stdoutTotal > 0 && stderrTotal > 0 ? 1 : 0);
|
|
391
|
+
return { stdout, stderr, exitCode: exit.exitCode, signal: exit.signal, totalLength };
|
|
85
392
|
}
|
|
86
393
|
async function main() {
|
|
87
394
|
const args = parseCliArgs(process.argv);
|
|
@@ -211,40 +518,69 @@ async function main() {
|
|
|
211
518
|
if (scriptNames.length === 0) {
|
|
212
519
|
console.error(`npm-run-mcp-server: No scripts found in ${pkgJsonPath}`);
|
|
213
520
|
}
|
|
521
|
+
const { config: mcpConfig, configPath: mcpConfigPath } = await readProjectMcpConfig(projectDir, verbose, typeof args.config === 'string' ? String(args.config) : undefined);
|
|
522
|
+
const filteredScriptNames = filterScriptNames(scriptNames, mcpConfig);
|
|
523
|
+
if (filteredScriptNames.length === 0) {
|
|
524
|
+
const hint = mcpConfig.include?.length
|
|
525
|
+
? 'Check your config "include"/"exclude" settings.'
|
|
526
|
+
: 'Check your package.json "scripts" section.';
|
|
527
|
+
console.error(`npm-run-mcp-server: No scripts selected for exposure. ${hint}`);
|
|
528
|
+
}
|
|
529
|
+
if (verbose && mcpConfig.include?.length) {
|
|
530
|
+
const missing = mcpConfig.include.filter((name) => !scripts[name]);
|
|
531
|
+
if (missing.length > 0) {
|
|
532
|
+
console.error(`[mcp] include list references missing scripts: ${missing.join(', ')}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const toolNameToScripts = new Map();
|
|
536
|
+
for (const scriptName of filteredScriptNames) {
|
|
537
|
+
const overrideName = mcpConfig.scripts?.[scriptName]?.toolName;
|
|
538
|
+
const toolName = normalizeToolName(overrideName ?? scriptName);
|
|
539
|
+
const existing = toolNameToScripts.get(toolName) ?? [];
|
|
540
|
+
existing.push(scriptName);
|
|
541
|
+
toolNameToScripts.set(toolName, existing);
|
|
542
|
+
}
|
|
543
|
+
const collisions = Array.from(toolNameToScripts.entries()).filter(([, names]) => names.length > 1);
|
|
544
|
+
if (collisions.length > 0) {
|
|
545
|
+
console.error('npm-run-mcp-server: Tool name collisions detected. Set "scripts.<name>.toolName" in npm-run-mcp.config.json to disambiguate.');
|
|
546
|
+
for (const [toolName, names] of collisions) {
|
|
547
|
+
console.error(` ${toolName}: ${names.join(', ')}`);
|
|
548
|
+
}
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
214
551
|
if (args['list-scripts']) {
|
|
215
|
-
for (const name of
|
|
552
|
+
for (const name of filteredScriptNames) {
|
|
216
553
|
console.error(`${name}: ${scripts[name]}`);
|
|
217
554
|
}
|
|
218
555
|
process.exit(0);
|
|
219
556
|
}
|
|
220
557
|
// Register a tool per script
|
|
221
|
-
for (const scriptName of
|
|
558
|
+
for (const scriptName of filteredScriptNames) {
|
|
222
559
|
// Sanitize tool name - MCP tools can only contain [a-z0-9_-]
|
|
223
|
-
const
|
|
560
|
+
const configForScript = mcpConfig.scripts?.[scriptName];
|
|
561
|
+
const toolName = normalizeToolName(configForScript?.toolName ?? scriptName);
|
|
224
562
|
// Create a more descriptive description
|
|
225
563
|
const scriptCommand = scripts[scriptName];
|
|
226
|
-
const description = `Run npm script "${scriptName}": ${scriptCommand}`;
|
|
227
|
-
server.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
description: 'Optional arguments appended after -- to the script'
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
}, async ({ args: extraArgs }) => {
|
|
238
|
-
const command = buildRunCommand(pm, scriptName, extraArgs);
|
|
564
|
+
const description = configForScript?.description ?? `Run npm script "${scriptName}": ${scriptCommand}`;
|
|
565
|
+
server.registerTool(toolName, {
|
|
566
|
+
description,
|
|
567
|
+
inputSchema: buildToolInputSchema(configForScript),
|
|
568
|
+
}, async (input) => {
|
|
569
|
+
const extraArgs = toolInputToExtraArgs(input);
|
|
570
|
+
const { command, args: runArgs } = buildRunCommand(pm, scriptName, extraArgs);
|
|
239
571
|
try {
|
|
240
|
-
const { stdout, stderr } = await
|
|
572
|
+
const { stdout, stderr, exitCode, signal, totalLength } = await runProcess(command, runArgs, {
|
|
241
573
|
cwd: projectDir,
|
|
242
574
|
env: process.env,
|
|
243
|
-
maxBuffer: 16 * 1024 * 1024, // 16MB
|
|
244
|
-
windowsHide: true,
|
|
245
575
|
});
|
|
246
576
|
const combined = stdout && stderr ? `${stdout}\n${stderr}` : stdout || stderr || '';
|
|
247
|
-
const
|
|
577
|
+
const succeeded = exitCode === 0;
|
|
578
|
+
const failurePrefix = succeeded
|
|
579
|
+
? ''
|
|
580
|
+
: `Command failed (exit=${exitCode}${signal ? `, signal=${signal}` : ''}): ${command} ${runArgs.join(' ')}`;
|
|
581
|
+
const combinedWithStatus = failurePrefix ? [failurePrefix, combined].filter(Boolean).join('\n') : combined;
|
|
582
|
+
const totalLengthWithStatus = failurePrefix ? totalLength + failurePrefix.length + (combined ? 1 : 0) : totalLength;
|
|
583
|
+
const { text } = trimOutput(combinedWithStatus, 12000, totalLengthWithStatus);
|
|
248
584
|
return {
|
|
249
585
|
content: [
|
|
250
586
|
{
|
|
@@ -255,11 +591,8 @@ async function main() {
|
|
|
255
591
|
};
|
|
256
592
|
}
|
|
257
593
|
catch (error) {
|
|
258
|
-
const stdout = error?.stdout ?? '';
|
|
259
|
-
const stderr = error?.stderr ?? '';
|
|
260
594
|
const message = error?.message ? String(error.message) : 'Script failed';
|
|
261
|
-
const
|
|
262
|
-
const { text } = trimOutput(combined);
|
|
595
|
+
const { text } = trimOutput(message);
|
|
263
596
|
return {
|
|
264
597
|
content: [
|
|
265
598
|
{
|
|
@@ -273,33 +606,43 @@ async function main() {
|
|
|
273
606
|
}
|
|
274
607
|
const transport = new StdioServerTransport();
|
|
275
608
|
if (verbose) {
|
|
276
|
-
console.error(`[mcp] registered ${
|
|
609
|
+
console.error(`[mcp] registered ${filteredScriptNames.length} tools; awaiting stdio client...`);
|
|
277
610
|
}
|
|
278
611
|
await server.connect(transport);
|
|
279
612
|
if (verbose) {
|
|
280
613
|
console.error(`[mcp] stdio transport connected (waiting for initialize)`);
|
|
281
614
|
}
|
|
282
|
-
// Set up file watcher for package
|
|
283
|
-
|
|
615
|
+
// Set up file watcher for package/config changes
|
|
616
|
+
const watchers = [];
|
|
617
|
+
const watchPath = (pathToWatch, label) => {
|
|
284
618
|
if (verbose) {
|
|
285
|
-
console.error(`[mcp] setting up file watcher for: ${
|
|
619
|
+
console.error(`[mcp] setting up file watcher for ${label}: ${pathToWatch}`);
|
|
286
620
|
}
|
|
287
|
-
|
|
288
|
-
if (eventType
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
// Gracefully exit to allow the MCP client to restart the server
|
|
293
|
-
process.exit(0);
|
|
621
|
+
watchers.push(watch(pathToWatch, (eventType) => {
|
|
622
|
+
if (eventType !== 'change')
|
|
623
|
+
return;
|
|
624
|
+
if (verbose) {
|
|
625
|
+
console.error(`[mcp] ${label} changed, restarting server...`);
|
|
294
626
|
}
|
|
295
|
-
|
|
296
|
-
|
|
627
|
+
// Gracefully exit to allow the MCP client to restart the server
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}));
|
|
630
|
+
};
|
|
631
|
+
if (pkgJsonPath)
|
|
632
|
+
watchPath(pkgJsonPath, 'package.json');
|
|
633
|
+
if (mcpConfigPath)
|
|
634
|
+
watchPath(mcpConfigPath, 'config');
|
|
635
|
+
if (watchers.length > 0) {
|
|
636
|
+
const cleanup = () => {
|
|
637
|
+
for (const watcher of watchers)
|
|
638
|
+
watcher.close();
|
|
639
|
+
};
|
|
297
640
|
process.on('SIGINT', () => {
|
|
298
|
-
|
|
641
|
+
cleanup();
|
|
299
642
|
process.exit(0);
|
|
300
643
|
});
|
|
301
644
|
process.on('SIGTERM', () => {
|
|
302
|
-
|
|
645
|
+
cleanup();
|
|
303
646
|
process.exit(0);
|
|
304
647
|
});
|
|
305
648
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/fstubner/npm-run-mcp-server/main/npm-run-mcp.config.schema.json",
|
|
4
|
+
"title": "npm-run-mcp-server config",
|
|
5
|
+
"description": "Controls which package.json scripts are exposed as MCP tools, plus optional per-script metadata.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"include": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": { "type": "string" },
|
|
12
|
+
"description": "Exact script names to include. If omitted, all scripts are eligible (subject to exclude)."
|
|
13
|
+
},
|
|
14
|
+
"exclude": {
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": { "type": "string" },
|
|
17
|
+
"description": "Exact script names to exclude."
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"description": "Per-script tool metadata overrides.",
|
|
22
|
+
"additionalProperties": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": false,
|
|
25
|
+
"properties": {
|
|
26
|
+
"toolName": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Override the generated tool name (after sanitization)."
|
|
29
|
+
},
|
|
30
|
+
"description": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Override the tool description."
|
|
33
|
+
},
|
|
34
|
+
"argsDescription": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Description shown for the `args` input field."
|
|
37
|
+
},
|
|
38
|
+
"inputSchema": {
|
|
39
|
+
"type": "object",
|
|
40
|
+
"description": "JSON Schema for additional tool inputs; keys are converted to CLI flags. `args` is always available."
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "npm-run-mcp-server",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
|
+
"mcpName": "io.github.fstubner/npm-run-mcp-server",
|
|
4
5
|
"description": "An MCP server that exposes package.json scripts as tools for agents.",
|
|
5
6
|
"bin": {
|
|
6
7
|
"npm-run-mcp-server": "dist/index.js"
|
|
@@ -11,19 +12,24 @@
|
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
13
14
|
"dist/index.js",
|
|
14
|
-
"dist/index.d.ts"
|
|
15
|
+
"dist/index.d.ts",
|
|
16
|
+
"dist/index.d.ts.map",
|
|
17
|
+
"npm-run-mcp.config.schema.json"
|
|
15
18
|
],
|
|
16
19
|
"scripts": {
|
|
17
20
|
"build": "node scripts/build.cjs",
|
|
18
21
|
"start": "node ./dist/index.js",
|
|
19
|
-
"test": "node
|
|
22
|
+
"test:integration": "node scripts/integration-test.mjs",
|
|
23
|
+
"test": "node dist/index.js --list-scripts && node scripts/integration-test.mjs && echo 'MCP server test completed successfully'",
|
|
20
24
|
"prepublishOnly": "npm run build"
|
|
21
25
|
},
|
|
22
26
|
"engines": {
|
|
23
27
|
"node": ">=18.18.0"
|
|
24
28
|
},
|
|
25
29
|
"dependencies": {
|
|
26
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
31
|
+
"cross-spawn": "^7.0.5",
|
|
32
|
+
"jsonc-parser": "^3.3.1",
|
|
27
33
|
"zod": "^3.23.8"
|
|
28
34
|
},
|
|
29
35
|
"bundledDependencies": [
|
|
@@ -31,7 +37,8 @@
|
|
|
31
37
|
],
|
|
32
38
|
"devDependencies": {
|
|
33
39
|
"typescript": "^5.4.0",
|
|
34
|
-
"@types/node": "^20.14.9"
|
|
40
|
+
"@types/node": "^20.14.9",
|
|
41
|
+
"@types/cross-spawn": "^6.0.6"
|
|
35
42
|
},
|
|
36
43
|
"keywords": [
|
|
37
44
|
"mcp",
|