securenow 7.3.0 → 7.5.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/mcp/catalog.js ADDED
@@ -0,0 +1,770 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEXT = 'text/plain';
7
+ const JSON_MIME = 'application/json';
8
+ const MARKDOWN = 'text/markdown';
9
+
10
+ function string(description) {
11
+ return { type: 'string', description };
12
+ }
13
+
14
+ function number(description, extra = {}) {
15
+ return { type: 'number', description, ...extra };
16
+ }
17
+
18
+ function boolean(description) {
19
+ return { type: 'boolean', description };
20
+ }
21
+
22
+ function arrayOfStrings(description) {
23
+ return { type: 'array', items: { type: 'string' }, description };
24
+ }
25
+
26
+ function objectSchema(properties, required = []) {
27
+ return {
28
+ type: 'object',
29
+ additionalProperties: false,
30
+ properties,
31
+ required,
32
+ };
33
+ }
34
+
35
+ function jsonText(value) {
36
+ return JSON.stringify(value, null, 2);
37
+ }
38
+
39
+ function maskSecret(value) {
40
+ if (!value) return null;
41
+ const text = String(value);
42
+ if (text.length <= 12) return '***';
43
+ return `${text.slice(0, 8)}...${text.slice(-4)}`;
44
+ }
45
+
46
+ function normalizeAppKeys(value) {
47
+ if (Array.isArray(value)) return value.filter(Boolean).join(',');
48
+ return value;
49
+ }
50
+
51
+ function sanitizeArgs(args = {}) {
52
+ const clone = { ...args };
53
+ for (const key of Object.keys(clone)) {
54
+ if (/token|apiKey|api_key|authorization|password|secret/i.test(key)) {
55
+ clone[key] = maskSecret(clone[key]);
56
+ }
57
+ }
58
+ return clone;
59
+ }
60
+
61
+ const confirmSchema = {
62
+ confirm: boolean('Required for write actions. Must be true.'),
63
+ reason: string('Short human-readable reason for the write action.'),
64
+ };
65
+
66
+ const appKeysInput = {
67
+ appKeys: {
68
+ oneOf: [
69
+ { type: 'string' },
70
+ { type: 'array', items: { type: 'string' } },
71
+ ],
72
+ description: 'Application key or comma-separated/list of application keys.',
73
+ },
74
+ };
75
+
76
+ const pagingInput = {
77
+ limit: number('Maximum results to return.', { minimum: 1, maximum: 500 }),
78
+ page: number('Page number.', { minimum: 1 }),
79
+ };
80
+
81
+ const timeRangeInput = {
82
+ from: string('Start time as ISO 8601, optional.'),
83
+ to: string('End time as ISO 8601, optional.'),
84
+ };
85
+
86
+ const TOOLS = [
87
+ {
88
+ name: 'securenow_auth_status',
89
+ title: 'SecureNow Auth Status',
90
+ description: 'Show local SecureNow credential source, selected app, and masked firewall key.',
91
+ scope: null,
92
+ localOnly: true,
93
+ readOnly: true,
94
+ inputSchema: objectSchema({}),
95
+ },
96
+ {
97
+ name: 'securenow_apps_list',
98
+ title: 'List Applications',
99
+ description: 'List SecureNow applications for the authenticated user.',
100
+ scope: 'applications:read',
101
+ readOnly: true,
102
+ method: 'GET',
103
+ endpoint: '/applications',
104
+ inputSchema: objectSchema({}),
105
+ },
106
+ {
107
+ name: 'securenow_apps_create',
108
+ title: 'Create Application',
109
+ description: 'Create a SecureNow application. Write action; requires confirmation.',
110
+ scope: 'applications:write',
111
+ readOnly: false,
112
+ confirm: true,
113
+ method: 'POST',
114
+ endpoint: '/applications',
115
+ bodyFields: ['name', 'hosts', 'instanceId'],
116
+ inputSchema: objectSchema({
117
+ name: string('Application name.'),
118
+ hosts: arrayOfStrings('Optional hostnames/domains for this application.'),
119
+ instanceId: string('Optional ClickHouse instance id.'),
120
+ ...confirmSchema,
121
+ }, ['name', 'confirm', 'reason']),
122
+ },
123
+ {
124
+ name: 'securenow_apps_info',
125
+ title: 'Get Application By Key',
126
+ description: 'Get details for an application by app key.',
127
+ scope: 'applications:read',
128
+ readOnly: true,
129
+ method: 'GET',
130
+ endpoint: '/applications/key/:key',
131
+ pathParams: ['key'],
132
+ inputSchema: objectSchema({
133
+ key: string('Application key UUID.'),
134
+ }, ['key']),
135
+ },
136
+ {
137
+ name: 'securenow_firewall_apps',
138
+ title: 'List Firewall Apps',
139
+ description: 'List applications with firewall toggle and SecureNow IPDB threshold state.',
140
+ scope: 'applications:read',
141
+ readOnly: true,
142
+ method: 'GET',
143
+ endpoint: '/firewall/apps',
144
+ inputSchema: objectSchema({}),
145
+ },
146
+ {
147
+ name: 'securenow_firewall_status',
148
+ title: 'Firewall Status',
149
+ description: 'Show firewall blocklist/allowlist status for the authenticated account.',
150
+ scope: 'firewall:read',
151
+ readOnly: true,
152
+ method: 'GET',
153
+ endpoint: '/firewall/status',
154
+ inputSchema: objectSchema({}),
155
+ },
156
+ {
157
+ name: 'securenow_firewall_enable',
158
+ title: 'Enable App Firewall',
159
+ description: 'Turn the per-app firewall ON. Write action; requires confirmation.',
160
+ scope: 'applications:write',
161
+ readOnly: false,
162
+ confirm: true,
163
+ method: 'PATCH',
164
+ endpoint: '/firewall/app/:appKey',
165
+ pathParams: ['appKey'],
166
+ fixedBody: { enabled: true },
167
+ inputSchema: objectSchema({
168
+ appKey: string('Application key UUID.'),
169
+ ...confirmSchema,
170
+ }, ['appKey', 'confirm', 'reason']),
171
+ },
172
+ {
173
+ name: 'securenow_firewall_disable',
174
+ title: 'Disable App Firewall',
175
+ description: 'Turn the per-app firewall OFF. Write action; requires confirmation.',
176
+ scope: 'applications:write',
177
+ readOnly: false,
178
+ destructive: true,
179
+ confirm: true,
180
+ method: 'PATCH',
181
+ endpoint: '/firewall/app/:appKey',
182
+ pathParams: ['appKey'],
183
+ fixedBody: { enabled: false },
184
+ inputSchema: objectSchema({
185
+ appKey: string('Application key UUID.'),
186
+ ...confirmSchema,
187
+ }, ['appKey', 'confirm', 'reason']),
188
+ },
189
+ {
190
+ name: 'securenow_firewall_set_threshold',
191
+ title: 'Set SecureNow IPDB Threshold',
192
+ description: 'Set the per-app SecureNow IPDB confidence threshold. Write action; requires confirmation.',
193
+ scope: 'applications:write',
194
+ readOnly: false,
195
+ confirm: true,
196
+ method: 'PATCH',
197
+ endpoint: '/firewall/app/:appKey',
198
+ pathParams: ['appKey'],
199
+ bodyFields: ['confidenceMinimum'],
200
+ inputSchema: objectSchema({
201
+ appKey: string('Application key UUID.'),
202
+ confidenceMinimum: number('Minimum SecureNow IPDB abuse confidence score.', { minimum: 0, maximum: 100 }),
203
+ ...confirmSchema,
204
+ }, ['appKey', 'confidenceMinimum', 'confirm', 'reason']),
205
+ },
206
+ {
207
+ name: 'securenow_firewall_test_ip',
208
+ title: 'Test Firewall IP Decision',
209
+ description: 'Check whether an IP would be blocked by the firewall.',
210
+ scope: 'firewall:read',
211
+ readOnly: true,
212
+ method: 'GET',
213
+ endpoint: '/firewall/check/:ip',
214
+ pathParams: ['ip'],
215
+ inputSchema: objectSchema({
216
+ ip: string('IPv4 address to test.'),
217
+ }, ['ip']),
218
+ },
219
+ {
220
+ name: 'securenow_traces_list',
221
+ title: 'List Traces',
222
+ description: 'List recent traces for one or more applications.',
223
+ scope: 'traces:read',
224
+ readOnly: true,
225
+ method: 'GET',
226
+ endpoint: '/traces',
227
+ queryFields: ['appKeys', 'from', 'to', 'limit'],
228
+ normalize: { appKeys: normalizeAppKeys },
229
+ inputSchema: objectSchema({
230
+ ...appKeysInput,
231
+ ...timeRangeInput,
232
+ limit: number('Maximum traces to return.', { minimum: 1, maximum: 200 }),
233
+ }, ['appKeys']),
234
+ },
235
+ {
236
+ name: 'securenow_traces_show',
237
+ title: 'Show Trace',
238
+ description: 'Show spans for a trace id scoped to one or more app keys.',
239
+ scope: 'traces:read',
240
+ readOnly: true,
241
+ method: 'GET',
242
+ endpoint: '/traces/:traceId',
243
+ pathParams: ['traceId'],
244
+ queryFields: ['appKeys'],
245
+ normalize: { appKeys: normalizeAppKeys },
246
+ inputSchema: objectSchema({
247
+ traceId: string('Trace id.'),
248
+ ...appKeysInput,
249
+ }, ['traceId', 'appKeys']),
250
+ },
251
+ {
252
+ name: 'securenow_logs_list',
253
+ title: 'List Logs',
254
+ description: 'List recent logs for one or more applications.',
255
+ scope: 'logs:read',
256
+ readOnly: true,
257
+ method: 'GET',
258
+ endpoint: '/logs',
259
+ queryFields: ['appKeys', 'from', 'to', 'severity', 'limit'],
260
+ normalize: { appKeys: normalizeAppKeys },
261
+ inputSchema: objectSchema({
262
+ ...appKeysInput,
263
+ ...timeRangeInput,
264
+ severity: string('Optional severity filter.'),
265
+ limit: number('Maximum logs to return.', { minimum: 1, maximum: 500 }),
266
+ }, ['appKeys']),
267
+ },
268
+ {
269
+ name: 'securenow_logs_for_trace',
270
+ title: 'Logs For Trace',
271
+ description: 'Show logs correlated to a trace id.',
272
+ scope: 'logs:read',
273
+ readOnly: true,
274
+ method: 'GET',
275
+ endpoint: '/logs/trace/:traceId',
276
+ pathParams: ['traceId'],
277
+ queryFields: ['appKeys'],
278
+ normalize: { appKeys: normalizeAppKeys },
279
+ inputSchema: objectSchema({
280
+ traceId: string('Trace id.'),
281
+ ...appKeysInput,
282
+ }, ['traceId', 'appKeys']),
283
+ },
284
+ {
285
+ name: 'securenow_notifications_list',
286
+ title: 'List Notifications',
287
+ description: 'List security notifications.',
288
+ scope: 'notifications:read',
289
+ readOnly: true,
290
+ method: 'GET',
291
+ endpoint: '/notifications',
292
+ queryFields: ['limit', 'page'],
293
+ inputSchema: objectSchema({ ...pagingInput }),
294
+ },
295
+ {
296
+ name: 'securenow_notifications_unread',
297
+ title: 'Unread Notification Count',
298
+ description: 'Return unread notification count.',
299
+ scope: 'notifications:read',
300
+ readOnly: true,
301
+ method: 'GET',
302
+ endpoint: '/notifications/unread-count',
303
+ inputSchema: objectSchema({}),
304
+ },
305
+ {
306
+ name: 'securenow_notifications_read',
307
+ title: 'Mark Notification Read',
308
+ description: 'Mark a notification as read. Write action; requires confirmation.',
309
+ scope: 'notifications:write',
310
+ readOnly: false,
311
+ confirm: true,
312
+ method: 'PUT',
313
+ endpoint: '/notifications/:id/read',
314
+ pathParams: ['id'],
315
+ inputSchema: objectSchema({
316
+ id: string('Notification id.'),
317
+ ...confirmSchema,
318
+ }, ['id', 'confirm', 'reason']),
319
+ },
320
+ {
321
+ name: 'securenow_ip_lookup',
322
+ title: 'IP Intelligence Lookup',
323
+ description: 'Look up SecureNow IP intelligence for an IP address.',
324
+ scope: 'ip_intel:read',
325
+ readOnly: true,
326
+ method: 'GET',
327
+ endpoint: '/ip/:ip',
328
+ pathParams: ['ip'],
329
+ inputSchema: objectSchema({
330
+ ip: string('IPv4 address.'),
331
+ }, ['ip']),
332
+ },
333
+ {
334
+ name: 'securenow_ip_traces',
335
+ title: 'IP Traces',
336
+ description: 'Fetch traces associated with an IP address.',
337
+ scope: 'ip_intel:read',
338
+ readOnly: true,
339
+ method: 'GET',
340
+ endpoint: '/ip/:ip/traces',
341
+ pathParams: ['ip'],
342
+ inputSchema: objectSchema({
343
+ ip: string('IPv4 address.'),
344
+ }, ['ip']),
345
+ },
346
+ {
347
+ name: 'securenow_forensics_query',
348
+ title: 'Run Forensics Query',
349
+ description: 'Run a natural-language forensic query. Write scope because it creates an async query job.',
350
+ scope: 'forensics:write',
351
+ readOnly: false,
352
+ confirm: true,
353
+ method: 'POST',
354
+ endpoint: '/forensics/query',
355
+ bodyFields: ['query', 'applicationId', 'instanceId'],
356
+ inputSchema: objectSchema({
357
+ query: string('Natural-language forensic query.'),
358
+ applicationId: string('Optional application database id.'),
359
+ instanceId: string('Optional ClickHouse instance id.'),
360
+ ...confirmSchema,
361
+ }, ['query', 'confirm', 'reason']),
362
+ },
363
+ {
364
+ name: 'securenow_forensics_library',
365
+ title: 'Forensics Query Library',
366
+ description: 'List saved forensics queries.',
367
+ scope: 'forensics:read',
368
+ readOnly: true,
369
+ method: 'GET',
370
+ endpoint: '/forensics/query-library',
371
+ inputSchema: objectSchema({}),
372
+ },
373
+ {
374
+ name: 'securenow_analytics_summary',
375
+ title: 'Analytics Summary',
376
+ description: 'Fetch response analytics summary.',
377
+ scope: 'analytics:read',
378
+ readOnly: true,
379
+ method: 'GET',
380
+ endpoint: '/analytics/summary',
381
+ queryFields: ['instanceId'],
382
+ inputSchema: objectSchema({
383
+ instanceId: string('Optional ClickHouse instance id.'),
384
+ }),
385
+ },
386
+ {
387
+ name: 'securenow_blocklist_list',
388
+ title: 'List Blocklist',
389
+ description: 'List blocked IPs.',
390
+ scope: 'blocklist:read',
391
+ readOnly: true,
392
+ method: 'GET',
393
+ endpoint: '/blocklist',
394
+ queryFields: ['page', 'limit'],
395
+ inputSchema: objectSchema({ ...pagingInput }),
396
+ },
397
+ {
398
+ name: 'securenow_blocklist_add',
399
+ title: 'Add Blocked IP',
400
+ description: 'Add an IP/CIDR to the blocklist. Write action; requires confirmation.',
401
+ scope: 'blocklist:write',
402
+ readOnly: false,
403
+ confirm: true,
404
+ method: 'POST',
405
+ endpoint: '/blocklist',
406
+ bodyFields: ['ip', 'reason', 'expiresAt', 'metadata'],
407
+ inputSchema: objectSchema({
408
+ ip: string('IPv4 address or CIDR.'),
409
+ reason: string('Reason for blocking.'),
410
+ expiresAt: string('Optional expiry time as ISO 8601.'),
411
+ metadata: { type: 'object', additionalProperties: true, description: 'Optional metadata.' },
412
+ ...confirmSchema,
413
+ }, ['ip', 'confirm', 'reason']),
414
+ },
415
+ {
416
+ name: 'securenow_blocklist_remove',
417
+ title: 'Remove Blocked IP',
418
+ description: 'Remove a blocklist entry. Write action; requires confirmation.',
419
+ scope: 'blocklist:write',
420
+ readOnly: false,
421
+ confirm: true,
422
+ method: 'DELETE',
423
+ endpoint: '/blocklist/:id',
424
+ pathParams: ['id'],
425
+ inputSchema: objectSchema({
426
+ id: string('Blocklist entry id.'),
427
+ ...confirmSchema,
428
+ }, ['id', 'confirm', 'reason']),
429
+ },
430
+ {
431
+ name: 'securenow_blocklist_stats',
432
+ title: 'Blocklist Stats',
433
+ description: 'Return blocklist statistics.',
434
+ scope: 'blocklist:read',
435
+ readOnly: true,
436
+ method: 'GET',
437
+ endpoint: '/blocklist/stats',
438
+ inputSchema: objectSchema({}),
439
+ },
440
+ {
441
+ name: 'securenow_allowlist_list',
442
+ title: 'List Allowlist',
443
+ description: 'List allowed IPs.',
444
+ scope: 'allowlist:read',
445
+ readOnly: true,
446
+ method: 'GET',
447
+ endpoint: '/allowlist',
448
+ queryFields: ['page', 'limit'],
449
+ inputSchema: objectSchema({ ...pagingInput }),
450
+ },
451
+ {
452
+ name: 'securenow_allowlist_add',
453
+ title: 'Add Allowed IP',
454
+ description: 'Add an IP/CIDR to the allowlist. Write action; requires confirmation.',
455
+ scope: 'allowlist:write',
456
+ readOnly: false,
457
+ confirm: true,
458
+ method: 'POST',
459
+ endpoint: '/allowlist',
460
+ bodyFields: ['ip', 'label', 'reason', 'expiresAt', 'applicationsAll', 'applicationKeys'],
461
+ inputSchema: objectSchema({
462
+ ip: string('IPv4 address or CIDR.'),
463
+ label: string('Human-readable label.'),
464
+ reason: string('Reason for allowing.'),
465
+ expiresAt: string('Optional expiry time as ISO 8601.'),
466
+ applicationsAll: boolean('Apply to all applications.'),
467
+ applicationKeys: arrayOfStrings('Application keys to scope this allowlist entry to.'),
468
+ ...confirmSchema,
469
+ }, ['ip', 'confirm', 'reason']),
470
+ },
471
+ {
472
+ name: 'securenow_allowlist_remove',
473
+ title: 'Remove Allowed IP',
474
+ description: 'Remove an allowlist entry. Write action; requires confirmation.',
475
+ scope: 'allowlist:write',
476
+ readOnly: false,
477
+ confirm: true,
478
+ method: 'DELETE',
479
+ endpoint: '/allowlist/:id',
480
+ pathParams: ['id'],
481
+ inputSchema: objectSchema({
482
+ id: string('Allowlist entry id.'),
483
+ ...confirmSchema,
484
+ }, ['id', 'confirm', 'reason']),
485
+ },
486
+ {
487
+ name: 'securenow_trusted_list',
488
+ title: 'List Trusted IPs',
489
+ description: 'List trusted IPs.',
490
+ scope: 'trusted_ips:read',
491
+ readOnly: true,
492
+ method: 'GET',
493
+ endpoint: '/trusted-ips',
494
+ inputSchema: objectSchema({}),
495
+ },
496
+ {
497
+ name: 'securenow_trusted_add',
498
+ title: 'Add Trusted IP',
499
+ description: 'Add a trusted IP/CIDR. Write action; requires confirmation.',
500
+ scope: 'trusted_ips:write',
501
+ readOnly: false,
502
+ confirm: true,
503
+ method: 'POST',
504
+ endpoint: '/trusted-ips',
505
+ bodyFields: ['ip', 'label', 'note', 'applicationsAll', 'applicationKeys'],
506
+ inputSchema: objectSchema({
507
+ ip: string('IPv4 address or CIDR.'),
508
+ label: string('Human-readable label.'),
509
+ note: string('Optional note.'),
510
+ applicationsAll: boolean('Apply to all applications.'),
511
+ applicationKeys: arrayOfStrings('Application keys to scope this trusted IP to.'),
512
+ ...confirmSchema,
513
+ }, ['ip', 'confirm', 'reason']),
514
+ },
515
+ {
516
+ name: 'securenow_trusted_remove',
517
+ title: 'Remove Trusted IP',
518
+ description: 'Remove a trusted IP entry. Write action; requires confirmation.',
519
+ scope: 'trusted_ips:write',
520
+ readOnly: false,
521
+ confirm: true,
522
+ method: 'DELETE',
523
+ endpoint: '/trusted-ips/:id',
524
+ pathParams: ['id'],
525
+ inputSchema: objectSchema({
526
+ id: string('Trusted IP entry id.'),
527
+ ...confirmSchema,
528
+ }, ['id', 'confirm', 'reason']),
529
+ },
530
+ ];
531
+
532
+ const RESOURCES = [
533
+ {
534
+ uri: 'securenow://docs/skill-api',
535
+ name: 'skill-api',
536
+ title: 'SecureNow SDK Skill',
537
+ description: 'Installed SDK skill instructions from SKILL-API.md.',
538
+ mimeType: MARKDOWN,
539
+ file: '../SKILL-API.md',
540
+ },
541
+ {
542
+ uri: 'securenow://docs/skill-cli',
543
+ name: 'skill-cli',
544
+ title: 'SecureNow CLI Skill',
545
+ description: 'Installed CLI skill instructions from SKILL-CLI.md.',
546
+ mimeType: MARKDOWN,
547
+ file: '../SKILL-CLI.md',
548
+ },
549
+ {
550
+ uri: 'securenow://docs/npm-readme',
551
+ name: 'npm-readme',
552
+ title: 'SecureNow npm README',
553
+ description: 'Installed npm README.',
554
+ mimeType: MARKDOWN,
555
+ file: '../NPM_README.md',
556
+ },
557
+ {
558
+ uri: 'securenow://project/config',
559
+ name: 'project-config',
560
+ title: 'SecureNow Project Config',
561
+ description: 'Resolved local SecureNow config and masked credentials.',
562
+ mimeType: JSON_MIME,
563
+ dynamic: 'projectConfig',
564
+ },
565
+ {
566
+ uri: 'securenow://tools/catalog',
567
+ name: 'tools-catalog',
568
+ title: 'SecureNow MCP Tool Catalog',
569
+ description: 'MCP tool names, scopes, and write/read classification.',
570
+ mimeType: JSON_MIME,
571
+ dynamic: 'toolsCatalog',
572
+ },
573
+ ];
574
+
575
+ const PROMPTS = [
576
+ {
577
+ name: 'secure_project_setup',
578
+ title: 'Secure Project Setup',
579
+ description: 'Generic framework setup prompt for SecureNow tracing, logs, body capture, multipart, and firewall defaults.',
580
+ arguments: [
581
+ { name: 'projectRoot', description: 'Project root to configure.', required: false },
582
+ ],
583
+ },
584
+ {
585
+ name: 'verify_firewall_default_on',
586
+ title: 'Verify Default-On Firewall',
587
+ description: 'Verify SecureNow CLI login, app firewall toggle, credentials, and SDK defaults.',
588
+ arguments: [
589
+ { name: 'appKey', description: 'Optional SecureNow app key.', required: false },
590
+ ],
591
+ },
592
+ {
593
+ name: 'investigate_ip',
594
+ title: 'Investigate IP',
595
+ description: 'Investigate an IP with SecureNow IP intelligence, traces, logs, and remediation options.',
596
+ arguments: [
597
+ { name: 'ip', description: 'IP address to investigate.', required: true },
598
+ { name: 'appKeys', description: 'Optional comma-separated app keys.', required: false },
599
+ ],
600
+ },
601
+ ];
602
+
603
+ function promptMessages(name, args = {}) {
604
+ if (name === 'secure_project_setup') {
605
+ return [
606
+ {
607
+ role: 'user',
608
+ content: {
609
+ type: 'text',
610
+ text: [
611
+ 'Set up SecureNow end-to-end for this JavaScript/TypeScript project.',
612
+ args.projectRoot ? `Project root: ${args.projectRoot}` : null,
613
+ 'Install or upgrade to securenow@7.5.0 or newer first.',
614
+ 'Run npx securenow whoami. If not logged in, run npx securenow login and stop until the browser flow completes.',
615
+ 'After login, verify whoami succeeds and that .securenow/credentials.json contains the selected app and firewall API key.',
616
+ 'Use the least invasive integration for the detected framework.',
617
+ 'Keep tracing, logs, POST body capture, multipart metadata capture, and the per-app firewall enabled by default.',
618
+ 'Verify with build/test plus npx securenow firewall apps/status and npx securenow env/doctor when available.',
619
+ ].filter(Boolean).join('\n'),
620
+ },
621
+ },
622
+ ];
623
+ }
624
+
625
+ if (name === 'verify_firewall_default_on') {
626
+ return [
627
+ {
628
+ role: 'user',
629
+ content: {
630
+ type: 'text',
631
+ text: [
632
+ 'Verify SecureNow default-on protection for this project.',
633
+ args.appKey ? `App key: ${args.appKey}` : null,
634
+ 'Check npx securenow whoami, npx securenow api-key show, npx securenow firewall apps, and npx securenow firewall status.',
635
+ 'Confirm traces, logs, SECURENOW_CAPTURE_BODY, SECURENOW_CAPTURE_MULTIPART, and firewall are enabled by default unless explicitly set to 0.',
636
+ 'Do not print full tokens or API keys.',
637
+ ].filter(Boolean).join('\n'),
638
+ },
639
+ },
640
+ ];
641
+ }
642
+
643
+ if (name === 'investigate_ip') {
644
+ return [
645
+ {
646
+ role: 'user',
647
+ content: {
648
+ type: 'text',
649
+ text: [
650
+ `Investigate IP ${args.ip || '<ip>'} with SecureNow.`,
651
+ args.appKeys ? `Scope to app keys: ${args.appKeys}` : null,
652
+ 'Use IP intelligence first, then related traces/logs, then recommend remediation.',
653
+ 'Only block, allow, or trust the IP after explicit user confirmation.',
654
+ ].filter(Boolean).join('\n'),
655
+ },
656
+ },
657
+ ];
658
+ }
659
+
660
+ throw new Error(`Unknown prompt: ${name}`);
661
+ }
662
+
663
+ function getTool(name) {
664
+ return TOOLS.find((tool) => tool.name === name) || null;
665
+ }
666
+
667
+ function mcpToolDefinition(tool) {
668
+ return {
669
+ name: tool.name,
670
+ title: tool.title,
671
+ description: tool.description,
672
+ inputSchema: tool.inputSchema || objectSchema({}),
673
+ annotations: {
674
+ readOnlyHint: !!tool.readOnly,
675
+ destructiveHint: !!tool.destructive,
676
+ idempotentHint: !!tool.readOnly,
677
+ },
678
+ };
679
+ }
680
+
681
+ function listTools() {
682
+ return TOOLS.map(mcpToolDefinition);
683
+ }
684
+
685
+ function listResources() {
686
+ return RESOURCES.map(({ file, dynamic, ...resource }) => resource);
687
+ }
688
+
689
+ function listPrompts() {
690
+ return PROMPTS;
691
+ }
692
+
693
+ function assertConfirmed(tool, args = {}) {
694
+ if (!tool.confirm) return;
695
+ if (args.confirm !== true) {
696
+ throw new Error(`${tool.name} is a write action. Pass confirm:true and a reason to proceed.`);
697
+ }
698
+ if (!args.reason || !String(args.reason).trim()) {
699
+ throw new Error(`${tool.name} requires a non-empty reason.`);
700
+ }
701
+ }
702
+
703
+ function buildApiRequest(tool, rawArgs = {}) {
704
+ const args = { ...rawArgs };
705
+ let endpoint = tool.endpoint;
706
+
707
+ for (const key of tool.pathParams || []) {
708
+ if (args[key] == null || args[key] === '') throw new Error(`Missing required argument: ${key}`);
709
+ endpoint = endpoint.replace(`:${key}`, encodeURIComponent(String(args[key])));
710
+ }
711
+
712
+ const query = {};
713
+ for (const key of tool.queryFields || []) {
714
+ let value = args[key];
715
+ if (tool.normalize && typeof tool.normalize[key] === 'function') {
716
+ value = tool.normalize[key](value);
717
+ }
718
+ if (value != null && value !== '') query[key] = value;
719
+ }
720
+
721
+ const body = { ...(tool.fixedBody || {}) };
722
+ for (const key of tool.bodyFields || []) {
723
+ let value = args[key];
724
+ if (tool.normalize && typeof tool.normalize[key] === 'function') {
725
+ value = tool.normalize[key](value);
726
+ }
727
+ if (value != null && value !== '') body[key] = value;
728
+ }
729
+
730
+ return {
731
+ method: tool.method,
732
+ endpoint,
733
+ query,
734
+ body: Object.keys(body).length > 0 ? body : null,
735
+ };
736
+ }
737
+
738
+ function readStaticResource(resource) {
739
+ const filepath = path.resolve(__dirname, resource.file);
740
+ return fs.readFileSync(filepath, 'utf8');
741
+ }
742
+
743
+ function resourceContent(resource, dynamicHandlers = {}) {
744
+ if (resource.file) return readStaticResource(resource);
745
+ if (resource.dynamic && dynamicHandlers[resource.dynamic]) {
746
+ return dynamicHandlers[resource.dynamic]();
747
+ }
748
+ throw new Error(`Resource is not readable: ${resource.uri}`);
749
+ }
750
+
751
+ module.exports = {
752
+ TOOLS,
753
+ RESOURCES,
754
+ PROMPTS,
755
+ TEXT,
756
+ JSON_MIME,
757
+ MARKDOWN,
758
+ getTool,
759
+ mcpToolDefinition,
760
+ listTools,
761
+ listResources,
762
+ listPrompts,
763
+ promptMessages,
764
+ assertConfirmed,
765
+ buildApiRequest,
766
+ resourceContent,
767
+ jsonText,
768
+ maskSecret,
769
+ sanitizeArgs,
770
+ };