@vettly/mcp 0.1.8 → 0.1.10

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.
@@ -15,7 +15,7 @@ var ModerateInputSchema = import_zod.z.object({
15
15
  policyId: import_zod.z.string().min(1).describe("The policy ID to use for moderation"),
16
16
  contentType: import_zod.z.enum(["text", "image", "video"]).default("text").describe("Type of content being moderated")
17
17
  });
18
- function registerModerateTool(server, client) {
18
+ function registerModerateTool(server, client, demoMode = false) {
19
19
  server.tool(
20
20
  "moderate_content",
21
21
  "Check text, image, or video content against a Vettly moderation policy. Returns safety assessment with category scores.",
@@ -24,6 +24,13 @@ function registerModerateTool(server, client) {
24
24
  policyId: ModerateInputSchema.shape.policyId,
25
25
  contentType: ModerateInputSchema.shape.contentType
26
26
  },
27
+ {
28
+ title: "Moderate Content",
29
+ readOnlyHint: true,
30
+ destructiveHint: false,
31
+ idempotentHint: true,
32
+ openWorldHint: true
33
+ },
27
34
  async (args) => {
28
35
  const input = ModerateInputSchema.parse(args);
29
36
  try {
@@ -32,24 +39,25 @@ function registerModerateTool(server, client) {
32
39
  policyId: input.policyId,
33
40
  contentType: input.contentType
34
41
  });
42
+ const text = JSON.stringify(
43
+ {
44
+ decisionId: result.decisionId,
45
+ safe: result.safe,
46
+ flagged: result.flagged,
47
+ action: result.action,
48
+ categories: result.categories,
49
+ provider: result.provider,
50
+ latency: result.latency,
51
+ cost: result.cost
52
+ },
53
+ null,
54
+ 2
55
+ );
35
56
  return {
36
57
  content: [
37
58
  {
38
59
  type: "text",
39
- text: JSON.stringify(
40
- {
41
- decisionId: result.decisionId,
42
- safe: result.safe,
43
- flagged: result.flagged,
44
- action: result.action,
45
- categories: result.categories,
46
- provider: result.provider,
47
- latency: result.latency,
48
- cost: result.cost
49
- },
50
- null,
51
- 2
52
- )
60
+ text: text + (demoMode ? DEMO_FOOTER : "")
53
61
  }
54
62
  ]
55
63
  };
@@ -73,22 +81,30 @@ var import_zod2 = require("zod");
73
81
  var ValidateInputSchema = import_zod2.z.object({
74
82
  yamlContent: import_zod2.z.string().min(1).describe("The YAML policy content to validate")
75
83
  });
76
- function registerValidateTool(server, client) {
84
+ function registerValidateTool(server, client, demoMode = false) {
77
85
  server.tool(
78
86
  "validate_policy",
79
87
  "Validate a Vettly policy YAML without saving it. Returns validation results with any syntax or configuration errors.",
80
88
  {
81
89
  yamlContent: ValidateInputSchema.shape.yamlContent
82
90
  },
91
+ {
92
+ title: "Validate Policy",
93
+ readOnlyHint: true,
94
+ destructiveHint: false,
95
+ idempotentHint: true,
96
+ openWorldHint: true
97
+ },
83
98
  async (args) => {
84
99
  const input = ValidateInputSchema.parse(args);
85
100
  try {
86
101
  const result = await client.validatePolicy(input.yamlContent);
102
+ const text = JSON.stringify(result, null, 2);
87
103
  return {
88
104
  content: [
89
105
  {
90
106
  type: "text",
91
- text: JSON.stringify(result, null, 2)
107
+ text: text + (demoMode ? DEMO_FOOTER : "")
92
108
  }
93
109
  ]
94
110
  };
@@ -108,19 +124,34 @@ function registerValidateTool(server, client) {
108
124
  }
109
125
 
110
126
  // src/tools/list-policies.ts
111
- function registerListPoliciesTool(server, client) {
127
+ var import_zod3 = require("zod");
128
+ function registerListPoliciesTool(server, client, demoMode = false) {
112
129
  server.tool(
113
130
  "list_policies",
114
131
  "List all moderation policies available in your Vettly account.",
115
- {},
116
- async () => {
132
+ {
133
+ status: import_zod3.z.enum(["active", "all"]).default("active").describe("Filter by policy status: active only or all policies")
134
+ },
135
+ {
136
+ title: "List Policies",
137
+ readOnlyHint: true,
138
+ destructiveHint: false,
139
+ idempotentHint: true,
140
+ openWorldHint: true
141
+ },
142
+ async (args) => {
117
143
  try {
118
144
  const result = await client.listPolicies();
145
+ let policies = result;
146
+ if (args.status === "active" && Array.isArray(policies)) {
147
+ policies = policies.filter((p) => p.status !== "archived");
148
+ }
149
+ const text = JSON.stringify(policies, null, 2);
119
150
  return {
120
151
  content: [
121
152
  {
122
153
  type: "text",
123
- text: JSON.stringify(result, null, 2)
154
+ text: text + (demoMode ? DEMO_FOOTER : "")
124
155
  }
125
156
  ]
126
157
  };
@@ -140,26 +171,34 @@ function registerListPoliciesTool(server, client) {
140
171
  }
141
172
 
142
173
  // src/tools/usage-stats.ts
143
- var import_zod3 = require("zod");
144
- var UsageStatsInputSchema = import_zod3.z.object({
145
- days: import_zod3.z.number().min(1).max(365).default(30).describe("Number of days to include in statistics")
174
+ var import_zod4 = require("zod");
175
+ var UsageStatsInputSchema = import_zod4.z.object({
176
+ days: import_zod4.z.number().min(1).max(365).default(30).describe("Number of days to include in statistics")
146
177
  });
147
- function registerUsageStatsTool(server, client) {
178
+ function registerUsageStatsTool(server, client, demoMode = false) {
148
179
  server.tool(
149
180
  "get_usage_stats",
150
181
  "Get usage statistics for your Vettly account including request counts, costs, and moderation outcomes.",
151
182
  {
152
183
  days: UsageStatsInputSchema.shape.days
153
184
  },
185
+ {
186
+ title: "Get Usage Stats",
187
+ readOnlyHint: true,
188
+ destructiveHint: false,
189
+ idempotentHint: true,
190
+ openWorldHint: true
191
+ },
154
192
  async (args) => {
155
193
  const input = UsageStatsInputSchema.parse(args);
156
194
  try {
157
195
  const result = await client.getUsageStats(input.days);
196
+ const text = JSON.stringify(result, null, 2);
158
197
  return {
159
198
  content: [
160
199
  {
161
200
  type: "text",
162
- text: JSON.stringify(result, null, 2)
201
+ text: text + (demoMode ? DEMO_FOOTER : "")
163
202
  }
164
203
  ]
165
204
  };
@@ -179,14 +218,14 @@ function registerUsageStatsTool(server, client) {
179
218
  }
180
219
 
181
220
  // src/tools/decisions.ts
182
- var import_zod4 = require("zod");
183
- var DecisionsInputSchema = import_zod4.z.object({
184
- limit: import_zod4.z.number().min(1).max(50).default(10).describe("Number of decisions to return"),
185
- flagged: import_zod4.z.boolean().optional().describe("Filter to only flagged content (true) or safe content (false)"),
186
- policyId: import_zod4.z.string().optional().describe("Filter by specific policy ID"),
187
- contentType: import_zod4.z.enum(["text", "image", "video"]).optional().describe("Filter by content type")
221
+ var import_zod5 = require("zod");
222
+ var DecisionsInputSchema = import_zod5.z.object({
223
+ limit: import_zod5.z.number().min(1).max(50).default(10).describe("Number of decisions to return"),
224
+ flagged: import_zod5.z.boolean().optional().describe("Filter to only flagged content (true) or safe content (false)"),
225
+ policyId: import_zod5.z.string().optional().describe("Filter by specific policy ID"),
226
+ contentType: import_zod5.z.enum(["text", "image", "video"]).optional().describe("Filter by content type")
188
227
  });
189
- function registerDecisionsTool(server, client) {
228
+ function registerDecisionsTool(server, client, demoMode = false) {
190
229
  server.tool(
191
230
  "get_recent_decisions",
192
231
  "Get recent moderation decisions with optional filtering by outcome, content type, or policy.",
@@ -196,6 +235,13 @@ function registerDecisionsTool(server, client) {
196
235
  policyId: DecisionsInputSchema.shape.policyId,
197
236
  contentType: DecisionsInputSchema.shape.contentType
198
237
  },
238
+ {
239
+ title: "Get Recent Decisions",
240
+ readOnlyHint: true,
241
+ destructiveHint: false,
242
+ idempotentHint: true,
243
+ openWorldHint: true
244
+ },
199
245
  async (args) => {
200
246
  const input = DecisionsInputSchema.parse(args);
201
247
  try {
@@ -210,11 +256,12 @@ function registerDecisionsTool(server, client) {
210
256
  if (input.contentType) {
211
257
  decisions = decisions.filter((d) => d.contentType === input.contentType);
212
258
  }
259
+ const text = JSON.stringify({ decisions }, null, 2);
213
260
  return {
214
261
  content: [
215
262
  {
216
263
  type: "text",
217
- text: JSON.stringify({ decisions }, null, 2)
264
+ text: text + (demoMode ? DEMO_FOOTER : "")
218
265
  }
219
266
  ]
220
267
  };
@@ -234,12 +281,13 @@ function registerDecisionsTool(server, client) {
234
281
  }
235
282
 
236
283
  // src/tools/index.ts
237
- function registerTools(server, client) {
238
- registerModerateTool(server, client);
239
- registerValidateTool(server, client);
240
- registerListPoliciesTool(server, client);
241
- registerUsageStatsTool(server, client);
242
- registerDecisionsTool(server, client);
284
+ var DEMO_FOOTER = "\n\n---\n\u{1F511} Demo mode (10 requests/day). Get your free API key at https://vettly.dev";
285
+ function registerTools(server, client, demoMode = false) {
286
+ registerModerateTool(server, client, demoMode);
287
+ registerValidateTool(server, client, demoMode);
288
+ registerListPoliciesTool(server, client, demoMode);
289
+ registerUsageStatsTool(server, client, demoMode);
290
+ registerDecisionsTool(server, client, demoMode);
243
291
  }
244
292
 
245
293
  // src/resources/policies.ts
@@ -316,18 +364,198 @@ function registerResources(server, client) {
316
364
  registerPolicyResources(server, client);
317
365
  }
318
366
 
367
+ // src/prompts/moderate-content-guide.ts
368
+ function registerModerateContentGuide(server) {
369
+ server.prompt(
370
+ "moderate-content-guide",
371
+ "Step-by-step guide to moderating content with Vettly. Explains the workflow: list policies, moderate content, and interpret results.",
372
+ async () => ({
373
+ messages: [
374
+ {
375
+ role: "user",
376
+ content: {
377
+ type: "text",
378
+ text: `You are helping a user moderate content with Vettly. Follow this workflow:
379
+
380
+ ## Step 1: List available policies
381
+ Use the \`list_policies\` tool to see what moderation policies are configured. Each policy has an ID and defines rules for what content is allowed or flagged.
382
+
383
+ ## Step 2: Moderate content
384
+ Use the \`moderate_content\` tool with:
385
+ - \`content\`: The text, image URL, or video URL to check
386
+ - \`policyId\`: The policy ID from step 1
387
+ - \`contentType\`: "text", "image", or "video"
388
+
389
+ ## Step 3: Interpret results
390
+ The response includes:
391
+ - \`safe\`: Boolean \u2014 whether the content passed moderation
392
+ - \`flagged\`: Boolean \u2014 whether the content was flagged for review
393
+ - \`action\`: The recommended action ("allow", "flag", or "block")
394
+ - \`categories\`: Breakdown of category scores (e.g. hate speech, violence, sexual content)
395
+ - \`decisionId\`: Unique ID for this decision, useful for auditing
396
+
397
+ ## Tips
398
+ - Always list policies first so you know which policy ID to use.
399
+ - For images and video, pass the URL as the \`content\` parameter.
400
+ - Use \`get_recent_decisions\` to review past moderation outcomes.
401
+ - Use \`get_usage_stats\` to check your account's usage and remaining quota.`
402
+ }
403
+ }
404
+ ]
405
+ })
406
+ );
407
+ }
408
+
409
+ // src/prompts/write-policy.ts
410
+ var import_zod6 = require("zod");
411
+ function registerWritePolicy(server) {
412
+ server.prompt(
413
+ "write-policy",
414
+ "Guide for writing a Vettly moderation policy in YAML. Optionally tailored to a specific platform type.",
415
+ { platform: import_zod6.z.string().optional().describe('The type of platform (e.g. "kids app", "marketplace", "social media", "dating app")') },
416
+ async (args) => {
417
+ const platformContext = args.platform ? `
418
+
419
+ The user is building a **${args.platform}**. Tailor your policy suggestions to this platform type \u2014 consider what content categories matter most and what thresholds are appropriate.` : "";
420
+ return {
421
+ messages: [
422
+ {
423
+ role: "user",
424
+ content: {
425
+ type: "text",
426
+ text: `You are helping a user write a Vettly moderation policy in YAML.${platformContext}
427
+
428
+ ## Policy YAML structure
429
+
430
+ A Vettly policy is a YAML document that defines moderation rules in plain English. Here's the structure:
431
+
432
+ \`\`\`yaml
433
+ name: My Policy
434
+ description: Brief description of what this policy covers
435
+
436
+ rules:
437
+ - category: hate_speech
438
+ threshold: 0.7
439
+ action: block
440
+ description: Block hateful content targeting protected groups
441
+
442
+ - category: violence
443
+ threshold: 0.8
444
+ action: flag
445
+ description: Flag violent content for human review
446
+
447
+ - category: sexual_content
448
+ threshold: 0.5
449
+ action: block
450
+ description: Block explicit sexual content
451
+
452
+ - category: self_harm
453
+ threshold: 0.6
454
+ action: block
455
+ description: Block content promoting self-harm
456
+ \`\`\`
457
+
458
+ ## Available categories
459
+ Common categories: \`hate_speech\`, \`violence\`, \`sexual_content\`, \`self_harm\`, \`harassment\`, \`dangerous_content\`, \`spam\`
460
+
461
+ ## Actions
462
+ - \`allow\` \u2014 let the content through
463
+ - \`flag\` \u2014 mark for human review
464
+ - \`block\` \u2014 reject the content
465
+
466
+ ## Thresholds
467
+ - 0.0\u20130.3: Very sensitive (catches more, higher false positive rate)
468
+ - 0.4\u20130.6: Moderate sensitivity
469
+ - 0.7\u20131.0: High confidence only (fewer false positives)
470
+
471
+ ## Workflow
472
+ 1. Draft the YAML policy based on the user's requirements
473
+ 2. Use the \`validate_policy\` tool to check for errors
474
+ 3. Iterate until validation passes
475
+ 4. The user can then save the policy via the Vettly dashboard`
476
+ }
477
+ }
478
+ ]
479
+ };
480
+ }
481
+ );
482
+ }
483
+
484
+ // src/prompts/review-flagged-content.ts
485
+ var import_zod7 = require("zod");
486
+ function registerReviewFlaggedContent(server) {
487
+ server.prompt(
488
+ "review-flagged-content",
489
+ "Template for reviewing flagged moderation decisions. Optionally filtered to a specific policy.",
490
+ { policyId: import_zod7.z.string().optional().describe("Filter flagged decisions to a specific policy ID") },
491
+ async (args) => {
492
+ const policyFilter = args.policyId ? `
493
+
494
+ Filter decisions to policy ID: \`${args.policyId}\`` : "";
495
+ return {
496
+ messages: [
497
+ {
498
+ role: "user",
499
+ content: {
500
+ type: "text",
501
+ text: `You are helping a user review flagged moderation decisions in Vettly.${policyFilter}
502
+
503
+ ## Review workflow
504
+
505
+ ### Step 1: Fetch flagged decisions
506
+ Use the \`get_recent_decisions\` tool with \`flagged: true\` to get recent flagged content.${args.policyId ? ` Also set \`policyId: "${args.policyId}"\` to filter by that policy.` : " Optionally filter by `policyId` or `contentType`."}
507
+
508
+ ### Step 2: Analyze each decision
509
+ For each flagged decision, examine:
510
+ - **Content**: What was submitted
511
+ - **Categories**: Which categories triggered and their scores
512
+ - **Action**: What action was taken (flag vs block)
513
+ - **Policy**: Which policy was applied
514
+
515
+ ### Step 3: Provide recommendations
516
+ For each flagged item, suggest one of:
517
+ - **Approve**: The content is acceptable, the flag was a false positive
518
+ - **Confirm block**: The content correctly violates the policy
519
+ - **Adjust policy**: The threshold may need tuning (too sensitive or not enough)
520
+
521
+ ### Step 4: Check for patterns
522
+ Use \`get_usage_stats\` to look at overall moderation trends. If many items are being flagged, the policy thresholds may need adjustment.
523
+
524
+ ## Tips
525
+ - High category scores (>0.9) are almost always correct flags
526
+ - Scores in the 0.5-0.7 range are where most false positives occur
527
+ - If a category consistently flags incorrectly, raise its threshold in the policy`
528
+ }
529
+ }
530
+ ]
531
+ };
532
+ }
533
+ );
534
+ }
535
+
536
+ // src/prompts/index.ts
537
+ function registerPrompts(server) {
538
+ registerModerateContentGuide(server);
539
+ registerWritePolicy(server);
540
+ registerReviewFlaggedContent(server);
541
+ }
542
+
319
543
  // src/server.ts
544
+ var DEMO_API_KEY = "vettly_test_demo_mcp_readonly";
320
545
  function createVettlyMcpServer(config) {
546
+ const demoMode = !config.apiKey;
547
+ const apiKey = config.apiKey || DEMO_API_KEY;
321
548
  const client = new import_sdk.ModerationClient({
322
- apiKey: config.apiKey,
549
+ apiKey,
323
550
  apiUrl: config.apiUrl
324
551
  });
325
552
  const server = new import_mcp2.McpServer({
326
553
  name: "vettly",
327
554
  version: "0.1.0"
328
555
  });
329
- registerTools(server, client);
556
+ registerTools(server, client, demoMode);
330
557
  registerResources(server, client);
558
+ registerPrompts(server);
331
559
  return server;
332
560
  }
333
561
 
@@ -335,22 +563,8 @@ function createVettlyMcpServer(config) {
335
563
  async function main() {
336
564
  const apiKey = process.env.VETTLY_API_KEY;
337
565
  if (!apiKey) {
338
- console.error("Error: VETTLY_API_KEY environment variable is required");
339
- console.error("");
340
- console.error("Usage:");
341
- console.error(" VETTLY_API_KEY=vettly_live_xxx npx @vettly/mcp");
342
- console.error("");
343
- console.error("Or configure in Claude Desktop:");
344
- console.error(" {");
345
- console.error(' "mcpServers": {');
346
- console.error(' "vettly": {');
347
- console.error(' "command": "npx",');
348
- console.error(' "args": ["-y", "@vettly/mcp"],');
349
- console.error(' "env": { "VETTLY_API_KEY": "vettly_live_xxx" }');
350
- console.error(" }");
351
- console.error(" }");
352
- console.error(" }");
353
- process.exit(1);
566
+ console.error("No VETTLY_API_KEY set \u2014 starting in demo mode (limited to 10 requests/day).");
567
+ console.error("Get your free API key at https://vettly.dev");
354
568
  }
355
569
  const server = createVettlyMcpServer({
356
570
  apiKey,
@@ -1,29 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createVettlyMcpServer
4
- } from "../chunk-VZJIPBOX.js";
4
+ } from "../chunk-PMBQ22Z2.js";
5
5
 
6
6
  // src/bin/vettly-mcp.ts
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
8
  async function main() {
9
9
  const apiKey = process.env.VETTLY_API_KEY;
10
10
  if (!apiKey) {
11
- console.error("Error: VETTLY_API_KEY environment variable is required");
12
- console.error("");
13
- console.error("Usage:");
14
- console.error(" VETTLY_API_KEY=vettly_live_xxx npx @vettly/mcp");
15
- console.error("");
16
- console.error("Or configure in Claude Desktop:");
17
- console.error(" {");
18
- console.error(' "mcpServers": {');
19
- console.error(' "vettly": {');
20
- console.error(' "command": "npx",');
21
- console.error(' "args": ["-y", "@vettly/mcp"],');
22
- console.error(' "env": { "VETTLY_API_KEY": "vettly_live_xxx" }');
23
- console.error(" }");
24
- console.error(" }");
25
- console.error(" }");
26
- process.exit(1);
11
+ console.error("No VETTLY_API_KEY set \u2014 starting in demo mode (limited to 10 requests/day).");
12
+ console.error("Get your free API key at https://vettly.dev");
27
13
  }
28
14
  const server = createVettlyMcpServer({
29
15
  apiKey,