@mctx-ai/mcp-dev 0.5.7 → 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 +3 -1
- package/package.json +6 -3
- package/src/server.js +92 -0
- package/src/watcher.js +3 -2
package/README.md
CHANGED
|
@@ -23,8 +23,10 @@ Your server restarts automatically on file changes.
|
|
|
23
23
|
|
|
24
24
|
## What It Does
|
|
25
25
|
|
|
26
|
-
- **Hot reload** — watches
|
|
26
|
+
- **Hot reload** — watches `.js`, `.mjs`, `.cjs`, and `.json` files and restarts on save
|
|
27
27
|
- **Request logging** — logs every MCP request and response to the console
|
|
28
|
+
- **Handler log surfacing** — prints any `log.*()` calls made inside your handlers to the dev console after each request
|
|
29
|
+
- **Sampling stub** — when a tool calls `ask()`, the `/_mctx/sampling` endpoint returns a clear error explaining that sampling is not supported in dev mode
|
|
28
30
|
- **Local testing** — serves your server over HTTP for use with any MCP client
|
|
29
31
|
|
|
30
32
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mctx-ai/mcp-dev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Local development server for @mctx-ai/mcp-server with hot reload",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"src"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "node --test",
|
|
17
|
+
"test": "node --test --test-force-exit",
|
|
18
18
|
"lint": "echo 'Linting not configured yet'"
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
@@ -35,11 +35,14 @@
|
|
|
35
35
|
"url": "https://github.com/mctx-ai/mcp-server/issues"
|
|
36
36
|
},
|
|
37
37
|
"homepage": "https://docs.mctx.ai",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.0.0"
|
|
40
|
+
},
|
|
38
41
|
"publishConfig": {
|
|
39
42
|
"access": "public",
|
|
40
43
|
"provenance": true
|
|
41
44
|
},
|
|
42
45
|
"peerDependencies": {
|
|
43
|
-
"@mctx-ai/mcp-server": "0.
|
|
46
|
+
"@mctx-ai/mcp-server": "^1.0.0"
|
|
44
47
|
}
|
|
45
48
|
}
|
package/src/server.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createServer } from "http";
|
|
12
|
+
import { getLogBuffer, clearLogBuffer } from "@mctx-ai/mcp-server";
|
|
12
13
|
import { watch } from "./watcher.js";
|
|
13
14
|
|
|
14
15
|
// ANSI color codes for logging
|
|
@@ -67,6 +68,23 @@ function formatMethod(rpcRequest) {
|
|
|
67
68
|
return `${method} (${params.name})`;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
// For completion/complete, show ref type and argument name
|
|
72
|
+
if (method === "completion/complete") {
|
|
73
|
+
const refType = params?.ref?.type;
|
|
74
|
+
const argName = params?.argument?.name;
|
|
75
|
+
if (refType && argName) {
|
|
76
|
+
return `${method} (${refType}, ${argName})`;
|
|
77
|
+
}
|
|
78
|
+
if (refType) {
|
|
79
|
+
return `${method} (${refType})`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// For logging/setLevel, show the requested level
|
|
84
|
+
if (method === "logging/setLevel" && params?.level) {
|
|
85
|
+
return `${method} (${params.level})`;
|
|
86
|
+
}
|
|
87
|
+
|
|
70
88
|
return method;
|
|
71
89
|
}
|
|
72
90
|
|
|
@@ -194,6 +212,41 @@ export async function startDevServer(entryUrl, port) {
|
|
|
194
212
|
|
|
195
213
|
// Create HTTP server
|
|
196
214
|
const server = createServer(async (req, res) => {
|
|
215
|
+
// Handle /_mctx/sampling back-channel: sampling is not supported in dev mode.
|
|
216
|
+
// Return a JSON-RPC error response so ask() fails with a clear message rather
|
|
217
|
+
// than a network error.
|
|
218
|
+
if (req.url === "/_mctx/sampling") {
|
|
219
|
+
let samplingBody = "";
|
|
220
|
+
req.on("data", (chunk) => {
|
|
221
|
+
samplingBody += chunk.toString();
|
|
222
|
+
});
|
|
223
|
+
req.on("end", () => {
|
|
224
|
+
let samplingId = null;
|
|
225
|
+
try {
|
|
226
|
+
const parsed = JSON.parse(samplingBody);
|
|
227
|
+
samplingId = parsed.id || null;
|
|
228
|
+
} catch {
|
|
229
|
+
// ignore parse errors — we still send a valid error response
|
|
230
|
+
}
|
|
231
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
232
|
+
res.end(
|
|
233
|
+
JSON.stringify({
|
|
234
|
+
jsonrpc: "2.0",
|
|
235
|
+
error: {
|
|
236
|
+
code: -32601,
|
|
237
|
+
message: "Sampling is not supported in dev mode — ask() will return null",
|
|
238
|
+
},
|
|
239
|
+
id: samplingId,
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
logFramework(
|
|
243
|
+
"Sampling request received — sampling is not supported in dev mode",
|
|
244
|
+
colors.yellow,
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
197
250
|
// Fix #2: If app failed to load initially, return error
|
|
198
251
|
if (!app) {
|
|
199
252
|
res.writeHead(503, { "Content-Type": "application/json" });
|
|
@@ -286,6 +339,23 @@ export async function startDevServer(entryUrl, port) {
|
|
|
286
339
|
return;
|
|
287
340
|
}
|
|
288
341
|
|
|
342
|
+
// Validate JSON-RPC method field before dispatching
|
|
343
|
+
if (!rpcRequest.method || typeof rpcRequest.method !== "string") {
|
|
344
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
345
|
+
res.end(
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
jsonrpc: "2.0",
|
|
348
|
+
error: {
|
|
349
|
+
code: -32600,
|
|
350
|
+
message: "Invalid Request - Missing or invalid method",
|
|
351
|
+
},
|
|
352
|
+
id: rpcRequest.id || null,
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
log(`${colors.red}✗${colors.reset} Invalid Request - missing method`, colors.red);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
289
359
|
// Log incoming request
|
|
290
360
|
const methodDisplay = formatMethod(rpcRequest);
|
|
291
361
|
log(`${colors.cyan}→${colors.reset} ${methodDisplay}`, colors.dim);
|
|
@@ -316,6 +386,28 @@ export async function startDevServer(entryUrl, port) {
|
|
|
316
386
|
const statusColor = statusCode >= 200 && statusCode < 300 ? colors.green : colors.red;
|
|
317
387
|
log(`${statusColor}←${colors.reset} ${statusCode} (${elapsed}ms)`, colors.dim);
|
|
318
388
|
|
|
389
|
+
// Surface buffered log entries from handler code to dev console
|
|
390
|
+
const bufferedLogs = getLogBuffer();
|
|
391
|
+
clearLogBuffer();
|
|
392
|
+
if (bufferedLogs.length > 0) {
|
|
393
|
+
for (const entry of bufferedLogs) {
|
|
394
|
+
const levelColor =
|
|
395
|
+
entry.level === "error" ||
|
|
396
|
+
entry.level === "critical" ||
|
|
397
|
+
entry.level === "alert" ||
|
|
398
|
+
entry.level === "emergency"
|
|
399
|
+
? colors.red
|
|
400
|
+
: entry.level === "warning"
|
|
401
|
+
? colors.yellow
|
|
402
|
+
: colors.dim;
|
|
403
|
+
const dataStr =
|
|
404
|
+
typeof entry.data === "string" ? entry.data : JSON.stringify(entry.data);
|
|
405
|
+
console.log(
|
|
406
|
+
`${colors.gray}[${timestamp()}]${colors.reset} ${levelColor}[log:${entry.level}]${colors.reset} ${dataStr}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
319
411
|
// Verbose logging: log full response body (skip initialize/initialized)
|
|
320
412
|
if (
|
|
321
413
|
isVerbose &&
|
package/src/watcher.js
CHANGED
|
@@ -104,8 +104,9 @@ export function watch(filePath, onChange) {
|
|
|
104
104
|
// Watch each directory
|
|
105
105
|
for (const { path, recursive } of watchDirs) {
|
|
106
106
|
const watcher = fsWatch(path, { recursive }, (eventType, changedFile) => {
|
|
107
|
-
// Only watch .js files
|
|
108
|
-
|
|
107
|
+
// Only watch .js, .mjs, .cjs, and .json files
|
|
108
|
+
const watchedExtensions = [".js", ".mjs", ".cjs", ".json"];
|
|
109
|
+
if (changedFile && !watchedExtensions.some((ext) => changedFile.endsWith(ext))) {
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
|