protect-mcp 0.2.1 → 0.3.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/dist/chunk-3WCA7O4D.mjs +977 -0
- package/dist/cli.js +760 -163
- package/dist/cli.mjs +241 -5
- package/dist/demo-server.d.mts +1 -0
- package/dist/demo-server.d.ts +1 -0
- package/dist/demo-server.js +137 -0
- package/dist/demo-server.mjs +136 -0
- package/dist/index.d.mts +75 -60
- package/dist/index.d.ts +75 -60
- package/dist/index.js +507 -269
- package/dist/index.mjs +3 -123
- package/package.json +4 -4
- package/dist/chunk-ZCKNFULF.mjs +0 -613
package/dist/cli.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
initSigning,
|
|
5
5
|
loadPolicy,
|
|
6
6
|
validateCredentials
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-3WCA7O4D.mjs";
|
|
8
8
|
|
|
9
9
|
// src/cli.ts
|
|
10
10
|
function printHelp() {
|
|
@@ -14,6 +14,8 @@ protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
|
14
14
|
Usage:
|
|
15
15
|
protect-mcp [options] -- <command> [args...]
|
|
16
16
|
protect-mcp init [--dir <path>]
|
|
17
|
+
protect-mcp demo
|
|
18
|
+
protect-mcp status [--dir <path>]
|
|
17
19
|
|
|
18
20
|
Options:
|
|
19
21
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -24,12 +26,16 @@ Options:
|
|
|
24
26
|
|
|
25
27
|
Commands:
|
|
26
28
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
29
|
+
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
30
|
+
status Show tool call statistics from the local decision log
|
|
27
31
|
|
|
28
32
|
Examples:
|
|
29
33
|
protect-mcp -- node my-server.js
|
|
30
34
|
protect-mcp --policy protect-mcp.json -- node my-server.js
|
|
31
35
|
protect-mcp --policy protect-mcp.json --enforce -- node my-server.js
|
|
32
36
|
protect-mcp init
|
|
37
|
+
protect-mcp demo
|
|
38
|
+
protect-mcp status
|
|
33
39
|
|
|
34
40
|
`);
|
|
35
41
|
}
|
|
@@ -144,19 +150,27 @@ async function handleInit(argv) {
|
|
|
144
150
|
},
|
|
145
151
|
credentials: {
|
|
146
152
|
_example_api: {
|
|
147
|
-
inject: "
|
|
148
|
-
name: "
|
|
153
|
+
inject: "env",
|
|
154
|
+
name: "EXAMPLE_API_KEY",
|
|
149
155
|
value_env: "EXAMPLE_API_KEY",
|
|
150
156
|
_comment: "Remove the underscore prefix and set EXAMPLE_API_KEY in your environment"
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
159
|
};
|
|
154
160
|
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
161
|
+
const claudeConfig = {
|
|
162
|
+
"mcpServers": {
|
|
163
|
+
"my-server": {
|
|
164
|
+
"command": "npx",
|
|
165
|
+
"args": ["protect-mcp", "--policy", configPath, "--", "node", "my-server.js"]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
155
169
|
process.stderr.write(`
|
|
156
170
|
${bold("protect-mcp initialized!")}
|
|
157
171
|
|
|
158
172
|
Created:
|
|
159
|
-
${configPath} Config with shadow mode +
|
|
173
|
+
${configPath} Config with shadow mode + local signing
|
|
160
174
|
${keyPath} Ed25519 signing keypair
|
|
161
175
|
|
|
162
176
|
${bold("Next steps:")}
|
|
@@ -170,13 +184,227 @@ ${bold("Your gateway public key:")}
|
|
|
170
184
|
${bold("Key ID (kid):")}
|
|
171
185
|
${keypair.kid}
|
|
172
186
|
|
|
187
|
+
${bold("Claude Desktop config snippet")} (add to claude_desktop_config.json):
|
|
188
|
+
${dim(JSON.stringify(claudeConfig, null, 2))}
|
|
189
|
+
|
|
190
|
+
${bold("Quick demo:")}
|
|
191
|
+
protect-mcp demo
|
|
192
|
+
|
|
173
193
|
Shadow mode is the default \u2014 all tool calls are logged and nothing is blocked.
|
|
174
|
-
|
|
194
|
+
Add --enforce when ready to block policy violations.
|
|
195
|
+
`);
|
|
196
|
+
}
|
|
197
|
+
async function handleDemo() {
|
|
198
|
+
const { existsSync } = await import("fs");
|
|
199
|
+
const { join, dirname } = await import("path");
|
|
200
|
+
const { fileURLToPath } = await import("url");
|
|
201
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
202
|
+
const __dirname = dirname(__filename);
|
|
203
|
+
const demoServerPath = join(__dirname, "demo-server.js");
|
|
204
|
+
const configPath = join(process.cwd(), "protect-mcp.json");
|
|
205
|
+
const hasConfig = existsSync(configPath);
|
|
206
|
+
if (!hasConfig) {
|
|
207
|
+
process.stderr.write(`
|
|
208
|
+
${bold("protect-mcp demo")}
|
|
209
|
+
|
|
210
|
+
Starting demo with default shadow mode (no signing).
|
|
211
|
+
For signed receipts, run ${dim("npx protect-mcp init")} first.
|
|
212
|
+
|
|
213
|
+
`);
|
|
214
|
+
} else {
|
|
215
|
+
process.stderr.write(`
|
|
216
|
+
${bold("protect-mcp demo")}
|
|
217
|
+
|
|
218
|
+
Using config from ${configPath}
|
|
219
|
+
Starting demo server with 5 tools...
|
|
220
|
+
|
|
221
|
+
`);
|
|
222
|
+
}
|
|
223
|
+
let policy = null;
|
|
224
|
+
let policyDigest = "none";
|
|
225
|
+
let credentials;
|
|
226
|
+
let signing;
|
|
227
|
+
if (hasConfig) {
|
|
228
|
+
try {
|
|
229
|
+
const loaded = loadPolicy(configPath);
|
|
230
|
+
policy = loaded.policy;
|
|
231
|
+
policyDigest = loaded.digest;
|
|
232
|
+
credentials = loaded.credentials;
|
|
233
|
+
signing = loaded.signing;
|
|
234
|
+
} catch (err) {
|
|
235
|
+
process.stderr.write(`[PROTECT_MCP] Warning: Could not load config: ${err instanceof Error ? err.message : err}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (signing) {
|
|
240
|
+
const warnings = await initSigning(signing);
|
|
241
|
+
for (const w of warnings) {
|
|
242
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
243
|
+
`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (credentials) {
|
|
247
|
+
const warnings = validateCredentials(credentials);
|
|
248
|
+
for (const w of warnings) {
|
|
249
|
+
process.stderr.write(`[PROTECT_MCP] Warning: ${w}
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const config = {
|
|
254
|
+
command: process.execPath,
|
|
255
|
+
// node
|
|
256
|
+
args: [demoServerPath],
|
|
257
|
+
policy,
|
|
258
|
+
policyDigest,
|
|
259
|
+
enforce: false,
|
|
260
|
+
// Demo always runs in shadow mode
|
|
261
|
+
verbose: true,
|
|
262
|
+
signing,
|
|
263
|
+
credentials
|
|
264
|
+
};
|
|
265
|
+
const gateway = new ProtectGateway(config);
|
|
266
|
+
process.stderr.write(`${bold("Demo ready!")} The demo server is running.
|
|
267
|
+
`);
|
|
268
|
+
process.stderr.write(`Send JSON-RPC tool calls on stdin, or use an MCP client.
|
|
269
|
+
|
|
270
|
+
`);
|
|
271
|
+
process.stderr.write(`${dim("Example (paste into stdin):")}
|
|
272
|
+
`);
|
|
273
|
+
process.stderr.write(`${dim('{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/etc/hosts"}}}')}
|
|
274
|
+
|
|
275
|
+
`);
|
|
276
|
+
await gateway.start();
|
|
277
|
+
}
|
|
278
|
+
async function handleStatus(argv) {
|
|
279
|
+
const { readFileSync, existsSync } = await import("fs");
|
|
280
|
+
const { join } = await import("path");
|
|
281
|
+
let dir = process.cwd();
|
|
282
|
+
const dirIdx = argv.indexOf("--dir");
|
|
283
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) {
|
|
284
|
+
dir = argv[dirIdx + 1];
|
|
285
|
+
}
|
|
286
|
+
const logPath = join(dir, ".protect-mcp-log.jsonl");
|
|
287
|
+
if (!existsSync(logPath)) {
|
|
288
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
289
|
+
|
|
290
|
+
`);
|
|
291
|
+
process.stderr.write(`No log file found at ${logPath}
|
|
292
|
+
`);
|
|
293
|
+
process.stderr.write(`Run protect-mcp with a wrapped server first to generate logs.
|
|
294
|
+
`);
|
|
295
|
+
process.exit(0);
|
|
296
|
+
}
|
|
297
|
+
const raw = readFileSync(logPath, "utf-8");
|
|
298
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
299
|
+
if (lines.length === 0) {
|
|
300
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
301
|
+
|
|
302
|
+
No entries in log file.
|
|
303
|
+
`);
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
const entries = [];
|
|
307
|
+
for (const line of lines) {
|
|
308
|
+
try {
|
|
309
|
+
entries.push(JSON.parse(line));
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (entries.length === 0) {
|
|
314
|
+
process.stderr.write(`${bold("protect-mcp status")}
|
|
315
|
+
|
|
316
|
+
No valid entries in log file.
|
|
317
|
+
`);
|
|
318
|
+
process.exit(0);
|
|
319
|
+
}
|
|
320
|
+
const toolCounts = /* @__PURE__ */ new Map();
|
|
321
|
+
let allowCount = 0;
|
|
322
|
+
let denyCount = 0;
|
|
323
|
+
let rateLimitCount = 0;
|
|
324
|
+
const tierCounts = /* @__PURE__ */ new Map();
|
|
325
|
+
const reasonCounts = /* @__PURE__ */ new Map();
|
|
326
|
+
for (const entry of entries) {
|
|
327
|
+
toolCounts.set(entry.tool, (toolCounts.get(entry.tool) || 0) + 1);
|
|
328
|
+
if (entry.decision === "allow") allowCount++;
|
|
329
|
+
else if (entry.decision === "deny") denyCount++;
|
|
330
|
+
if (entry.reason_code === "rate_limit_exceeded") rateLimitCount++;
|
|
331
|
+
if (entry.tier) tierCounts.set(entry.tier, (tierCounts.get(entry.tier) || 0) + 1);
|
|
332
|
+
reasonCounts.set(entry.reason_code, (reasonCounts.get(entry.reason_code) || 0) + 1);
|
|
333
|
+
}
|
|
334
|
+
const firstTs = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
335
|
+
const lastTs = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
336
|
+
const sortedTools = [...toolCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
337
|
+
process.stdout.write(`
|
|
338
|
+
${bold("protect-mcp status")}
|
|
339
|
+
|
|
340
|
+
`);
|
|
341
|
+
process.stdout.write(` Total decisions: ${bold(String(entries.length))}
|
|
342
|
+
`);
|
|
343
|
+
process.stdout.write(` ${green("\u2713 Allow")}: ${allowCount} ${red("\u2717 Deny")}: ${denyCount} ${yellow("\u2298 Rate-limited")}: ${rateLimitCount}
|
|
344
|
+
|
|
345
|
+
`);
|
|
346
|
+
process.stdout.write(` ${bold("Time range:")}
|
|
347
|
+
`);
|
|
348
|
+
process.stdout.write(` First: ${firstTs.toISOString()}
|
|
349
|
+
`);
|
|
350
|
+
process.stdout.write(` Last: ${lastTs.toISOString()}
|
|
351
|
+
|
|
352
|
+
`);
|
|
353
|
+
process.stdout.write(` ${bold("Top tools:")}
|
|
354
|
+
`);
|
|
355
|
+
for (const [tool, count] of sortedTools.slice(0, 10)) {
|
|
356
|
+
const bar = "\u2588".repeat(Math.min(Math.ceil(count / entries.length * 30), 30));
|
|
357
|
+
process.stdout.write(` ${tool.padEnd(20)} ${String(count).padStart(4)} ${dim(bar)}
|
|
358
|
+
`);
|
|
359
|
+
}
|
|
360
|
+
if (tierCounts.size > 0) {
|
|
361
|
+
process.stdout.write(`
|
|
362
|
+
${bold("Trust tiers seen:")}
|
|
363
|
+
`);
|
|
364
|
+
for (const [tier, count] of tierCounts) {
|
|
365
|
+
process.stdout.write(` ${tier.padEnd(15)} ${count}
|
|
366
|
+
`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
process.stdout.write(`
|
|
370
|
+
${bold("Decision reasons:")}
|
|
371
|
+
`);
|
|
372
|
+
for (const [reason, count] of [...reasonCounts.entries()].sort((a, b) => b[1] - a[1])) {
|
|
373
|
+
process.stdout.write(` ${reason.padEnd(25)} ${count}
|
|
374
|
+
`);
|
|
375
|
+
}
|
|
376
|
+
const evidencePath = join(dir, ".protect-mcp-evidence.json");
|
|
377
|
+
if (existsSync(evidencePath)) {
|
|
378
|
+
try {
|
|
379
|
+
const evidenceRaw = readFileSync(evidencePath, "utf-8");
|
|
380
|
+
const evidence = JSON.parse(evidenceRaw);
|
|
381
|
+
const agentCount = Object.keys(evidence.agents || {}).length;
|
|
382
|
+
process.stdout.write(`
|
|
383
|
+
${bold("Evidence store:")} ${agentCount} agent(s) tracked
|
|
384
|
+
`);
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(`
|
|
389
|
+
Log file: ${dim(logPath)}
|
|
390
|
+
|
|
175
391
|
`);
|
|
176
392
|
}
|
|
177
393
|
function bold(s) {
|
|
178
394
|
return process.env.NO_COLOR ? s : `\x1B[1m${s}\x1B[0m`;
|
|
179
395
|
}
|
|
396
|
+
function dim(s) {
|
|
397
|
+
return process.env.NO_COLOR ? s : `\x1B[2m${s}\x1B[0m`;
|
|
398
|
+
}
|
|
399
|
+
function green(s) {
|
|
400
|
+
return process.env.NO_COLOR ? s : `\x1B[32m${s}\x1B[0m`;
|
|
401
|
+
}
|
|
402
|
+
function red(s) {
|
|
403
|
+
return process.env.NO_COLOR ? s : `\x1B[31m${s}\x1B[0m`;
|
|
404
|
+
}
|
|
405
|
+
function yellow(s) {
|
|
406
|
+
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
407
|
+
}
|
|
180
408
|
async function main() {
|
|
181
409
|
const args = process.argv.slice(2);
|
|
182
410
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -187,6 +415,14 @@ async function main() {
|
|
|
187
415
|
await handleInit(args.slice(1));
|
|
188
416
|
process.exit(0);
|
|
189
417
|
}
|
|
418
|
+
if (args[0] === "demo") {
|
|
419
|
+
await handleDemo();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (args[0] === "status") {
|
|
423
|
+
await handleStatus(args.slice(1));
|
|
424
|
+
process.exit(0);
|
|
425
|
+
}
|
|
190
426
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
191
427
|
let policy = null;
|
|
192
428
|
let policyDigest = "none";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/demo-server.ts
|
|
5
|
+
var import_node_readline = require("readline");
|
|
6
|
+
var TOOLS = [
|
|
7
|
+
{
|
|
8
|
+
name: "read_file",
|
|
9
|
+
description: "Read the contents of a file",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: "object",
|
|
12
|
+
properties: { path: { type: "string", description: "File path to read" } },
|
|
13
|
+
required: ["path"]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "write_file",
|
|
18
|
+
description: "Write content to a file",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
path: { type: "string", description: "File path to write" },
|
|
23
|
+
content: { type: "string", description: "Content to write" }
|
|
24
|
+
},
|
|
25
|
+
required: ["path", "content"]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "delete_file",
|
|
30
|
+
description: "Delete a file from the filesystem",
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: { path: { type: "string", description: "File path to delete" } },
|
|
34
|
+
required: ["path"]
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "web_search",
|
|
39
|
+
description: "Search the web for information",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { query: { type: "string", description: "Search query" } },
|
|
43
|
+
required: ["query"]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "deploy",
|
|
48
|
+
description: "Deploy the application to production",
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: "object",
|
|
51
|
+
properties: {
|
|
52
|
+
environment: { type: "string", description: "Target environment", enum: ["staging", "production"] },
|
|
53
|
+
reason: { type: "string", description: "Deployment reason" }
|
|
54
|
+
},
|
|
55
|
+
required: ["environment"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
function handleRequest(request) {
|
|
60
|
+
if (request.method === "initialize") {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
jsonrpc: "2.0",
|
|
63
|
+
id: request.id,
|
|
64
|
+
result: {
|
|
65
|
+
protocolVersion: "2024-11-05",
|
|
66
|
+
serverInfo: { name: "protect-mcp-demo", version: "0.2.0" },
|
|
67
|
+
capabilities: { tools: {} }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (request.method === "notifications/initialized") {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
if (request.method === "tools/list") {
|
|
75
|
+
return JSON.stringify({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: request.id,
|
|
78
|
+
result: { tools: TOOLS }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (request.method === "tools/call") {
|
|
82
|
+
const toolName = request.params?.name || "unknown";
|
|
83
|
+
const args = request.params?.arguments || {};
|
|
84
|
+
let resultText;
|
|
85
|
+
switch (toolName) {
|
|
86
|
+
case "read_file":
|
|
87
|
+
resultText = `[demo] Read file: ${args.path || "/example.txt"}
|
|
88
|
+
Contents: Hello from protect-mcp demo server!`;
|
|
89
|
+
break;
|
|
90
|
+
case "write_file":
|
|
91
|
+
resultText = `[demo] Wrote ${String(args.content || "").length} bytes to ${args.path || "/example.txt"}`;
|
|
92
|
+
break;
|
|
93
|
+
case "delete_file":
|
|
94
|
+
resultText = `[demo] Deleted file: ${args.path || "/example.txt"}`;
|
|
95
|
+
break;
|
|
96
|
+
case "web_search":
|
|
97
|
+
resultText = `[demo] Search results for "${args.query || "test"}":
|
|
98
|
+
1. Example result \u2014 scopeblind.com
|
|
99
|
+
2. MCP security \u2014 modelcontextprotocol.io`;
|
|
100
|
+
break;
|
|
101
|
+
case "deploy":
|
|
102
|
+
resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
resultText = `[demo] Unknown tool: ${toolName}`;
|
|
106
|
+
}
|
|
107
|
+
return JSON.stringify({
|
|
108
|
+
jsonrpc: "2.0",
|
|
109
|
+
id: request.id,
|
|
110
|
+
result: {
|
|
111
|
+
content: [{ type: "text", text: resultText }]
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (request.id !== void 0) {
|
|
116
|
+
return JSON.stringify({
|
|
117
|
+
jsonrpc: "2.0",
|
|
118
|
+
id: request.id,
|
|
119
|
+
error: { code: -32601, message: `Method not found: ${request.method}` }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
var rl = (0, import_node_readline.createInterface)({ input: process.stdin, crlfDelay: Infinity });
|
|
125
|
+
rl.on("line", (line) => {
|
|
126
|
+
const trimmed = line.trim();
|
|
127
|
+
if (!trimmed) return;
|
|
128
|
+
try {
|
|
129
|
+
const request = JSON.parse(trimmed);
|
|
130
|
+
const response = handleRequest(request);
|
|
131
|
+
if (response) {
|
|
132
|
+
process.stdout.write(response + "\n");
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/demo-server.ts
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
var TOOLS = [
|
|
6
|
+
{
|
|
7
|
+
name: "read_file",
|
|
8
|
+
description: "Read the contents of a file",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: { path: { type: "string", description: "File path to read" } },
|
|
12
|
+
required: ["path"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "write_file",
|
|
17
|
+
description: "Write content to a file",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
path: { type: "string", description: "File path to write" },
|
|
22
|
+
content: { type: "string", description: "Content to write" }
|
|
23
|
+
},
|
|
24
|
+
required: ["path", "content"]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "delete_file",
|
|
29
|
+
description: "Delete a file from the filesystem",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: { path: { type: "string", description: "File path to delete" } },
|
|
33
|
+
required: ["path"]
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "web_search",
|
|
38
|
+
description: "Search the web for information",
|
|
39
|
+
inputSchema: {
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { query: { type: "string", description: "Search query" } },
|
|
42
|
+
required: ["query"]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "deploy",
|
|
47
|
+
description: "Deploy the application to production",
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: "object",
|
|
50
|
+
properties: {
|
|
51
|
+
environment: { type: "string", description: "Target environment", enum: ["staging", "production"] },
|
|
52
|
+
reason: { type: "string", description: "Deployment reason" }
|
|
53
|
+
},
|
|
54
|
+
required: ["environment"]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
];
|
|
58
|
+
function handleRequest(request) {
|
|
59
|
+
if (request.method === "initialize") {
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
jsonrpc: "2.0",
|
|
62
|
+
id: request.id,
|
|
63
|
+
result: {
|
|
64
|
+
protocolVersion: "2024-11-05",
|
|
65
|
+
serverInfo: { name: "protect-mcp-demo", version: "0.2.0" },
|
|
66
|
+
capabilities: { tools: {} }
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (request.method === "notifications/initialized") {
|
|
71
|
+
return "";
|
|
72
|
+
}
|
|
73
|
+
if (request.method === "tools/list") {
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
jsonrpc: "2.0",
|
|
76
|
+
id: request.id,
|
|
77
|
+
result: { tools: TOOLS }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (request.method === "tools/call") {
|
|
81
|
+
const toolName = request.params?.name || "unknown";
|
|
82
|
+
const args = request.params?.arguments || {};
|
|
83
|
+
let resultText;
|
|
84
|
+
switch (toolName) {
|
|
85
|
+
case "read_file":
|
|
86
|
+
resultText = `[demo] Read file: ${args.path || "/example.txt"}
|
|
87
|
+
Contents: Hello from protect-mcp demo server!`;
|
|
88
|
+
break;
|
|
89
|
+
case "write_file":
|
|
90
|
+
resultText = `[demo] Wrote ${String(args.content || "").length} bytes to ${args.path || "/example.txt"}`;
|
|
91
|
+
break;
|
|
92
|
+
case "delete_file":
|
|
93
|
+
resultText = `[demo] Deleted file: ${args.path || "/example.txt"}`;
|
|
94
|
+
break;
|
|
95
|
+
case "web_search":
|
|
96
|
+
resultText = `[demo] Search results for "${args.query || "test"}":
|
|
97
|
+
1. Example result \u2014 scopeblind.com
|
|
98
|
+
2. MCP security \u2014 modelcontextprotocol.io`;
|
|
99
|
+
break;
|
|
100
|
+
case "deploy":
|
|
101
|
+
resultText = `[demo] Deployed to ${args.environment || "staging"}${args.reason ? ` (reason: ${args.reason})` : ""}`;
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
resultText = `[demo] Unknown tool: ${toolName}`;
|
|
105
|
+
}
|
|
106
|
+
return JSON.stringify({
|
|
107
|
+
jsonrpc: "2.0",
|
|
108
|
+
id: request.id,
|
|
109
|
+
result: {
|
|
110
|
+
content: [{ type: "text", text: resultText }]
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (request.id !== void 0) {
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
jsonrpc: "2.0",
|
|
117
|
+
id: request.id,
|
|
118
|
+
error: { code: -32601, message: `Method not found: ${request.method}` }
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
var rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
124
|
+
rl.on("line", (line) => {
|
|
125
|
+
const trimmed = line.trim();
|
|
126
|
+
if (!trimmed) return;
|
|
127
|
+
try {
|
|
128
|
+
const request = JSON.parse(trimmed);
|
|
129
|
+
const response = handleRequest(request);
|
|
130
|
+
if (response) {
|
|
131
|
+
process.stdout.write(response + "\n");
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
process.stderr.write("[DEMO_SERVER] protect-mcp demo server started \u2014 5 tools registered\n");
|