@vettly/mcp 0.1.1
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 +122 -0
- package/dist/bin/vettly-mcp.cjs +365 -0
- package/dist/bin/vettly-mcp.d.cts +1 -0
- package/dist/bin/vettly-mcp.d.ts +1 -0
- package/dist/bin/vettly-mcp.js +38 -0
- package/dist/chunk-VZJIPBOX.js +330 -0
- package/dist/index.cjs +356 -0
- package/dist/index.d.cts +9 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/package.json +76 -0
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# @vettly/mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for Vettly content moderation API. Enables AI assistants like Claude to moderate content, validate policies, and review moderation decisions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vettly/mcp
|
|
9
|
+
# or
|
|
10
|
+
npx @vettly/mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
### Claude Desktop
|
|
16
|
+
|
|
17
|
+
Add to your `claude_desktop_config.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"vettly": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["-y", "@vettly/mcp"],
|
|
25
|
+
"env": {
|
|
26
|
+
"VETTLY_API_KEY": "vettly_live_xxx"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Environment Variables
|
|
34
|
+
|
|
35
|
+
- `VETTLY_API_KEY` (required) - Your Vettly API key
|
|
36
|
+
- `VETTLY_API_URL` (optional) - API URL (defaults to `https://api.vettly.dev`)
|
|
37
|
+
|
|
38
|
+
## Available Tools
|
|
39
|
+
|
|
40
|
+
### `moderate_content`
|
|
41
|
+
|
|
42
|
+
Check content against a Vettly moderation policy.
|
|
43
|
+
|
|
44
|
+
**Parameters:**
|
|
45
|
+
- `content` (required) - The content to moderate
|
|
46
|
+
- `policyId` (required) - The policy ID to use
|
|
47
|
+
- `contentType` (optional) - `text`, `image`, or `video` (default: `text`)
|
|
48
|
+
|
|
49
|
+
**Example:**
|
|
50
|
+
```
|
|
51
|
+
Moderate this text: "Hello world" using policy "default-policy"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `validate_policy`
|
|
55
|
+
|
|
56
|
+
Validate a policy YAML without saving it.
|
|
57
|
+
|
|
58
|
+
**Parameters:**
|
|
59
|
+
- `yamlContent` (required) - The YAML policy content to validate
|
|
60
|
+
|
|
61
|
+
**Example:**
|
|
62
|
+
```
|
|
63
|
+
Validate this policy YAML:
|
|
64
|
+
name: my-policy
|
|
65
|
+
rules:
|
|
66
|
+
- category: hate_speech
|
|
67
|
+
threshold: 0.7
|
|
68
|
+
action: block
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `list_policies`
|
|
72
|
+
|
|
73
|
+
List all available moderation policies in your account.
|
|
74
|
+
|
|
75
|
+
**Parameters:** None
|
|
76
|
+
|
|
77
|
+
### `get_usage_stats`
|
|
78
|
+
|
|
79
|
+
Get usage statistics including request counts, costs, and moderation outcomes.
|
|
80
|
+
|
|
81
|
+
**Parameters:**
|
|
82
|
+
- `days` (optional) - Number of days to include (1-365, default: 30)
|
|
83
|
+
|
|
84
|
+
### `get_recent_decisions`
|
|
85
|
+
|
|
86
|
+
Get recent moderation decisions with optional filtering.
|
|
87
|
+
|
|
88
|
+
**Parameters:**
|
|
89
|
+
- `limit` (optional) - Number of decisions to return (1-50, default: 10)
|
|
90
|
+
- `flagged` (optional) - Filter by flagged status
|
|
91
|
+
- `policyId` (optional) - Filter by policy ID
|
|
92
|
+
- `contentType` (optional) - Filter by content type
|
|
93
|
+
|
|
94
|
+
## Resources
|
|
95
|
+
|
|
96
|
+
### `vettly://policies`
|
|
97
|
+
|
|
98
|
+
Read-only list of all available moderation policies.
|
|
99
|
+
|
|
100
|
+
### `vettly://policies/{policyId}`
|
|
101
|
+
|
|
102
|
+
Read-only access to a specific policy's YAML configuration.
|
|
103
|
+
|
|
104
|
+
## Programmatic Usage
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import { createVettlyMcpServer } from '@vettly/mcp';
|
|
108
|
+
|
|
109
|
+
const server = createVettlyMcpServer({
|
|
110
|
+
apiKey: process.env.VETTLY_API_KEY!,
|
|
111
|
+
apiUrl: 'https://api.vettly.dev', // optional
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Connect to your transport
|
|
115
|
+
server.connect(transport);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Links
|
|
119
|
+
|
|
120
|
+
- [Vettly Documentation](https://docs.vettly.dev)
|
|
121
|
+
- [Get an API Key](https://vettly.dev)
|
|
122
|
+
- [MCP Specification](https://modelcontextprotocol.io)
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/bin/vettly-mcp.ts
|
|
5
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
|
|
7
|
+
// src/server.ts
|
|
8
|
+
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
var import_sdk = require("@vettly/sdk");
|
|
10
|
+
|
|
11
|
+
// src/tools/moderate.ts
|
|
12
|
+
var import_zod = require("zod");
|
|
13
|
+
var ModerateInputSchema = import_zod.z.object({
|
|
14
|
+
content: import_zod.z.string().min(1).max(1e5).describe("The content to moderate (text content, or URL for images/video)"),
|
|
15
|
+
policyId: import_zod.z.string().min(1).describe("The policy ID to use for moderation"),
|
|
16
|
+
contentType: import_zod.z.enum(["text", "image", "video"]).default("text").describe("Type of content being moderated")
|
|
17
|
+
});
|
|
18
|
+
function registerModerateTool(server, client) {
|
|
19
|
+
server.tool(
|
|
20
|
+
"moderate_content",
|
|
21
|
+
"Check text, image, or video content against a Vettly moderation policy. Returns safety assessment with category scores.",
|
|
22
|
+
{
|
|
23
|
+
content: ModerateInputSchema.shape.content,
|
|
24
|
+
policyId: ModerateInputSchema.shape.policyId,
|
|
25
|
+
contentType: ModerateInputSchema.shape.contentType
|
|
26
|
+
},
|
|
27
|
+
async (args) => {
|
|
28
|
+
const input = ModerateInputSchema.parse(args);
|
|
29
|
+
try {
|
|
30
|
+
const result = await client.check({
|
|
31
|
+
content: input.content,
|
|
32
|
+
policyId: input.policyId,
|
|
33
|
+
contentType: input.contentType
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
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
|
+
)
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: `Moderation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
isError: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/tools/validate.ts
|
|
72
|
+
var import_zod2 = require("zod");
|
|
73
|
+
var ValidateInputSchema = import_zod2.z.object({
|
|
74
|
+
yamlContent: import_zod2.z.string().min(1).describe("The YAML policy content to validate")
|
|
75
|
+
});
|
|
76
|
+
function registerValidateTool(server, client) {
|
|
77
|
+
server.tool(
|
|
78
|
+
"validate_policy",
|
|
79
|
+
"Validate a Vettly policy YAML without saving it. Returns validation results with any syntax or configuration errors.",
|
|
80
|
+
{
|
|
81
|
+
yamlContent: ValidateInputSchema.shape.yamlContent
|
|
82
|
+
},
|
|
83
|
+
async (args) => {
|
|
84
|
+
const input = ValidateInputSchema.parse(args);
|
|
85
|
+
try {
|
|
86
|
+
const result = await client.validatePolicy(input.yamlContent);
|
|
87
|
+
return {
|
|
88
|
+
content: [
|
|
89
|
+
{
|
|
90
|
+
type: "text",
|
|
91
|
+
text: JSON.stringify(result, null, 2)
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
};
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
isError: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/tools/list-policies.ts
|
|
111
|
+
function registerListPoliciesTool(server, client) {
|
|
112
|
+
server.tool(
|
|
113
|
+
"list_policies",
|
|
114
|
+
"List all moderation policies available in your Vettly account.",
|
|
115
|
+
{},
|
|
116
|
+
async () => {
|
|
117
|
+
try {
|
|
118
|
+
const result = await client.listPolicies();
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: JSON.stringify(result, null, 2)
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
isError: true
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 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")
|
|
146
|
+
});
|
|
147
|
+
function registerUsageStatsTool(server, client) {
|
|
148
|
+
server.tool(
|
|
149
|
+
"get_usage_stats",
|
|
150
|
+
"Get usage statistics for your Vettly account including request counts, costs, and moderation outcomes.",
|
|
151
|
+
{
|
|
152
|
+
days: UsageStatsInputSchema.shape.days
|
|
153
|
+
},
|
|
154
|
+
async (args) => {
|
|
155
|
+
const input = UsageStatsInputSchema.parse(args);
|
|
156
|
+
try {
|
|
157
|
+
const result = await client.getUsageStats(input.days);
|
|
158
|
+
return {
|
|
159
|
+
content: [
|
|
160
|
+
{
|
|
161
|
+
type: "text",
|
|
162
|
+
text: JSON.stringify(result, null, 2)
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
166
|
+
} catch (error) {
|
|
167
|
+
return {
|
|
168
|
+
content: [
|
|
169
|
+
{
|
|
170
|
+
type: "text",
|
|
171
|
+
text: `Failed to get usage stats: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
172
|
+
}
|
|
173
|
+
],
|
|
174
|
+
isError: true
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 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")
|
|
188
|
+
});
|
|
189
|
+
function registerDecisionsTool(server, client) {
|
|
190
|
+
server.tool(
|
|
191
|
+
"get_recent_decisions",
|
|
192
|
+
"Get recent moderation decisions with optional filtering by outcome, content type, or policy.",
|
|
193
|
+
{
|
|
194
|
+
limit: DecisionsInputSchema.shape.limit,
|
|
195
|
+
flagged: DecisionsInputSchema.shape.flagged,
|
|
196
|
+
policyId: DecisionsInputSchema.shape.policyId,
|
|
197
|
+
contentType: DecisionsInputSchema.shape.contentType
|
|
198
|
+
},
|
|
199
|
+
async (args) => {
|
|
200
|
+
const input = DecisionsInputSchema.parse(args);
|
|
201
|
+
try {
|
|
202
|
+
const result = await client.listDecisions({ limit: input.limit });
|
|
203
|
+
let decisions = result.decisions || result;
|
|
204
|
+
if (input.flagged !== void 0) {
|
|
205
|
+
decisions = decisions.filter((d) => d.flagged === input.flagged);
|
|
206
|
+
}
|
|
207
|
+
if (input.policyId) {
|
|
208
|
+
decisions = decisions.filter((d) => d.policyId === input.policyId);
|
|
209
|
+
}
|
|
210
|
+
if (input.contentType) {
|
|
211
|
+
decisions = decisions.filter((d) => d.contentType === input.contentType);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
content: [
|
|
215
|
+
{
|
|
216
|
+
type: "text",
|
|
217
|
+
text: JSON.stringify({ decisions }, null, 2)
|
|
218
|
+
}
|
|
219
|
+
]
|
|
220
|
+
};
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: "text",
|
|
226
|
+
text: `Failed to get decisions: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
isError: true
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 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);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/resources/policies.ts
|
|
246
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
247
|
+
function registerPolicyResources(server, client) {
|
|
248
|
+
server.resource(
|
|
249
|
+
"policies",
|
|
250
|
+
"vettly://policies",
|
|
251
|
+
{
|
|
252
|
+
description: "List of all available moderation policies",
|
|
253
|
+
mimeType: "application/json"
|
|
254
|
+
},
|
|
255
|
+
async (uri) => {
|
|
256
|
+
try {
|
|
257
|
+
const result = await client.listPolicies();
|
|
258
|
+
return {
|
|
259
|
+
contents: [
|
|
260
|
+
{
|
|
261
|
+
uri: uri.href,
|
|
262
|
+
mimeType: "application/json",
|
|
263
|
+
text: JSON.stringify(result, null, 2)
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
};
|
|
267
|
+
} catch (error) {
|
|
268
|
+
return {
|
|
269
|
+
contents: [
|
|
270
|
+
{
|
|
271
|
+
uri: uri.href,
|
|
272
|
+
mimeType: "text/plain",
|
|
273
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
server.resource(
|
|
281
|
+
"policy",
|
|
282
|
+
new import_mcp.ResourceTemplate("vettly://policies/{policyId}", { list: void 0 }),
|
|
283
|
+
{
|
|
284
|
+
description: "A specific moderation policy with its YAML configuration",
|
|
285
|
+
mimeType: "application/x-yaml"
|
|
286
|
+
},
|
|
287
|
+
async (uri, { policyId }) => {
|
|
288
|
+
try {
|
|
289
|
+
const result = await client.getPolicy(policyId);
|
|
290
|
+
return {
|
|
291
|
+
contents: [
|
|
292
|
+
{
|
|
293
|
+
uri: uri.href,
|
|
294
|
+
mimeType: "application/x-yaml",
|
|
295
|
+
text: result.yamlContent || result.yaml || JSON.stringify(result, null, 2)
|
|
296
|
+
}
|
|
297
|
+
]
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
return {
|
|
301
|
+
contents: [
|
|
302
|
+
{
|
|
303
|
+
uri: uri.href,
|
|
304
|
+
mimeType: "text/plain",
|
|
305
|
+
text: `Failed to get policy: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// src/resources/index.ts
|
|
315
|
+
function registerResources(server, client) {
|
|
316
|
+
registerPolicyResources(server, client);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/server.ts
|
|
320
|
+
function createVettlyMcpServer(config) {
|
|
321
|
+
const client = new import_sdk.ModerationClient({
|
|
322
|
+
apiKey: config.apiKey,
|
|
323
|
+
apiUrl: config.apiUrl
|
|
324
|
+
});
|
|
325
|
+
const server = new import_mcp2.McpServer({
|
|
326
|
+
name: "vettly",
|
|
327
|
+
version: "0.1.0"
|
|
328
|
+
});
|
|
329
|
+
registerTools(server, client);
|
|
330
|
+
registerResources(server, client);
|
|
331
|
+
return server;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/bin/vettly-mcp.ts
|
|
335
|
+
async function main() {
|
|
336
|
+
const apiKey = process.env.VETTLY_API_KEY;
|
|
337
|
+
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);
|
|
354
|
+
}
|
|
355
|
+
const server = createVettlyMcpServer({
|
|
356
|
+
apiKey,
|
|
357
|
+
apiUrl: process.env.VETTLY_API_URL
|
|
358
|
+
});
|
|
359
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
360
|
+
await server.connect(transport);
|
|
361
|
+
}
|
|
362
|
+
main().catch((error) => {
|
|
363
|
+
console.error("Fatal error:", error);
|
|
364
|
+
process.exit(1);
|
|
365
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createVettlyMcpServer
|
|
4
|
+
} from "../chunk-VZJIPBOX.js";
|
|
5
|
+
|
|
6
|
+
// src/bin/vettly-mcp.ts
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const apiKey = process.env.VETTLY_API_KEY;
|
|
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);
|
|
27
|
+
}
|
|
28
|
+
const server = createVettlyMcpServer({
|
|
29
|
+
apiKey,
|
|
30
|
+
apiUrl: process.env.VETTLY_API_URL
|
|
31
|
+
});
|
|
32
|
+
const transport = new StdioServerTransport();
|
|
33
|
+
await server.connect(transport);
|
|
34
|
+
}
|
|
35
|
+
main().catch((error) => {
|
|
36
|
+
console.error("Fatal error:", error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
});
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { ModerationClient } from "@vettly/sdk";
|
|
4
|
+
|
|
5
|
+
// src/tools/moderate.ts
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
var ModerateInputSchema = z.object({
|
|
8
|
+
content: z.string().min(1).max(1e5).describe("The content to moderate (text content, or URL for images/video)"),
|
|
9
|
+
policyId: z.string().min(1).describe("The policy ID to use for moderation"),
|
|
10
|
+
contentType: z.enum(["text", "image", "video"]).default("text").describe("Type of content being moderated")
|
|
11
|
+
});
|
|
12
|
+
function registerModerateTool(server, client) {
|
|
13
|
+
server.tool(
|
|
14
|
+
"moderate_content",
|
|
15
|
+
"Check text, image, or video content against a Vettly moderation policy. Returns safety assessment with category scores.",
|
|
16
|
+
{
|
|
17
|
+
content: ModerateInputSchema.shape.content,
|
|
18
|
+
policyId: ModerateInputSchema.shape.policyId,
|
|
19
|
+
contentType: ModerateInputSchema.shape.contentType
|
|
20
|
+
},
|
|
21
|
+
async (args) => {
|
|
22
|
+
const input = ModerateInputSchema.parse(args);
|
|
23
|
+
try {
|
|
24
|
+
const result = await client.check({
|
|
25
|
+
content: input.content,
|
|
26
|
+
policyId: input.policyId,
|
|
27
|
+
contentType: input.contentType
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.stringify(
|
|
34
|
+
{
|
|
35
|
+
decisionId: result.decisionId,
|
|
36
|
+
safe: result.safe,
|
|
37
|
+
flagged: result.flagged,
|
|
38
|
+
action: result.action,
|
|
39
|
+
categories: result.categories,
|
|
40
|
+
provider: result.provider,
|
|
41
|
+
latency: result.latency,
|
|
42
|
+
cost: result.cost
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Moderation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
isError: true
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/tools/validate.ts
|
|
66
|
+
import { z as z2 } from "zod";
|
|
67
|
+
var ValidateInputSchema = z2.object({
|
|
68
|
+
yamlContent: z2.string().min(1).describe("The YAML policy content to validate")
|
|
69
|
+
});
|
|
70
|
+
function registerValidateTool(server, client) {
|
|
71
|
+
server.tool(
|
|
72
|
+
"validate_policy",
|
|
73
|
+
"Validate a Vettly policy YAML without saving it. Returns validation results with any syntax or configuration errors.",
|
|
74
|
+
{
|
|
75
|
+
yamlContent: ValidateInputSchema.shape.yamlContent
|
|
76
|
+
},
|
|
77
|
+
async (args) => {
|
|
78
|
+
const input = ValidateInputSchema.parse(args);
|
|
79
|
+
try {
|
|
80
|
+
const result = await client.validatePolicy(input.yamlContent);
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: JSON.stringify(result, null, 2)
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return {
|
|
91
|
+
content: [
|
|
92
|
+
{
|
|
93
|
+
type: "text",
|
|
94
|
+
text: `Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
isError: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/tools/list-policies.ts
|
|
105
|
+
function registerListPoliciesTool(server, client) {
|
|
106
|
+
server.tool(
|
|
107
|
+
"list_policies",
|
|
108
|
+
"List all moderation policies available in your Vettly account.",
|
|
109
|
+
{},
|
|
110
|
+
async () => {
|
|
111
|
+
try {
|
|
112
|
+
const result = await client.listPolicies();
|
|
113
|
+
return {
|
|
114
|
+
content: [
|
|
115
|
+
{
|
|
116
|
+
type: "text",
|
|
117
|
+
text: JSON.stringify(result, null, 2)
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
127
|
+
}
|
|
128
|
+
],
|
|
129
|
+
isError: true
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/tools/usage-stats.ts
|
|
137
|
+
import { z as z3 } from "zod";
|
|
138
|
+
var UsageStatsInputSchema = z3.object({
|
|
139
|
+
days: z3.number().min(1).max(365).default(30).describe("Number of days to include in statistics")
|
|
140
|
+
});
|
|
141
|
+
function registerUsageStatsTool(server, client) {
|
|
142
|
+
server.tool(
|
|
143
|
+
"get_usage_stats",
|
|
144
|
+
"Get usage statistics for your Vettly account including request counts, costs, and moderation outcomes.",
|
|
145
|
+
{
|
|
146
|
+
days: UsageStatsInputSchema.shape.days
|
|
147
|
+
},
|
|
148
|
+
async (args) => {
|
|
149
|
+
const input = UsageStatsInputSchema.parse(args);
|
|
150
|
+
try {
|
|
151
|
+
const result = await client.getUsageStats(input.days);
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: JSON.stringify(result, null, 2)
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
} catch (error) {
|
|
161
|
+
return {
|
|
162
|
+
content: [
|
|
163
|
+
{
|
|
164
|
+
type: "text",
|
|
165
|
+
text: `Failed to get usage stats: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
166
|
+
}
|
|
167
|
+
],
|
|
168
|
+
isError: true
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/tools/decisions.ts
|
|
176
|
+
import { z as z4 } from "zod";
|
|
177
|
+
var DecisionsInputSchema = z4.object({
|
|
178
|
+
limit: z4.number().min(1).max(50).default(10).describe("Number of decisions to return"),
|
|
179
|
+
flagged: z4.boolean().optional().describe("Filter to only flagged content (true) or safe content (false)"),
|
|
180
|
+
policyId: z4.string().optional().describe("Filter by specific policy ID"),
|
|
181
|
+
contentType: z4.enum(["text", "image", "video"]).optional().describe("Filter by content type")
|
|
182
|
+
});
|
|
183
|
+
function registerDecisionsTool(server, client) {
|
|
184
|
+
server.tool(
|
|
185
|
+
"get_recent_decisions",
|
|
186
|
+
"Get recent moderation decisions with optional filtering by outcome, content type, or policy.",
|
|
187
|
+
{
|
|
188
|
+
limit: DecisionsInputSchema.shape.limit,
|
|
189
|
+
flagged: DecisionsInputSchema.shape.flagged,
|
|
190
|
+
policyId: DecisionsInputSchema.shape.policyId,
|
|
191
|
+
contentType: DecisionsInputSchema.shape.contentType
|
|
192
|
+
},
|
|
193
|
+
async (args) => {
|
|
194
|
+
const input = DecisionsInputSchema.parse(args);
|
|
195
|
+
try {
|
|
196
|
+
const result = await client.listDecisions({ limit: input.limit });
|
|
197
|
+
let decisions = result.decisions || result;
|
|
198
|
+
if (input.flagged !== void 0) {
|
|
199
|
+
decisions = decisions.filter((d) => d.flagged === input.flagged);
|
|
200
|
+
}
|
|
201
|
+
if (input.policyId) {
|
|
202
|
+
decisions = decisions.filter((d) => d.policyId === input.policyId);
|
|
203
|
+
}
|
|
204
|
+
if (input.contentType) {
|
|
205
|
+
decisions = decisions.filter((d) => d.contentType === input.contentType);
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: JSON.stringify({ decisions }, null, 2)
|
|
212
|
+
}
|
|
213
|
+
]
|
|
214
|
+
};
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: `Failed to get decisions: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
isError: true
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/tools/index.ts
|
|
231
|
+
function registerTools(server, client) {
|
|
232
|
+
registerModerateTool(server, client);
|
|
233
|
+
registerValidateTool(server, client);
|
|
234
|
+
registerListPoliciesTool(server, client);
|
|
235
|
+
registerUsageStatsTool(server, client);
|
|
236
|
+
registerDecisionsTool(server, client);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/resources/policies.ts
|
|
240
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
241
|
+
function registerPolicyResources(server, client) {
|
|
242
|
+
server.resource(
|
|
243
|
+
"policies",
|
|
244
|
+
"vettly://policies",
|
|
245
|
+
{
|
|
246
|
+
description: "List of all available moderation policies",
|
|
247
|
+
mimeType: "application/json"
|
|
248
|
+
},
|
|
249
|
+
async (uri) => {
|
|
250
|
+
try {
|
|
251
|
+
const result = await client.listPolicies();
|
|
252
|
+
return {
|
|
253
|
+
contents: [
|
|
254
|
+
{
|
|
255
|
+
uri: uri.href,
|
|
256
|
+
mimeType: "application/json",
|
|
257
|
+
text: JSON.stringify(result, null, 2)
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
};
|
|
261
|
+
} catch (error) {
|
|
262
|
+
return {
|
|
263
|
+
contents: [
|
|
264
|
+
{
|
|
265
|
+
uri: uri.href,
|
|
266
|
+
mimeType: "text/plain",
|
|
267
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
268
|
+
}
|
|
269
|
+
]
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
server.resource(
|
|
275
|
+
"policy",
|
|
276
|
+
new ResourceTemplate("vettly://policies/{policyId}", { list: void 0 }),
|
|
277
|
+
{
|
|
278
|
+
description: "A specific moderation policy with its YAML configuration",
|
|
279
|
+
mimeType: "application/x-yaml"
|
|
280
|
+
},
|
|
281
|
+
async (uri, { policyId }) => {
|
|
282
|
+
try {
|
|
283
|
+
const result = await client.getPolicy(policyId);
|
|
284
|
+
return {
|
|
285
|
+
contents: [
|
|
286
|
+
{
|
|
287
|
+
uri: uri.href,
|
|
288
|
+
mimeType: "application/x-yaml",
|
|
289
|
+
text: result.yamlContent || result.yaml || JSON.stringify(result, null, 2)
|
|
290
|
+
}
|
|
291
|
+
]
|
|
292
|
+
};
|
|
293
|
+
} catch (error) {
|
|
294
|
+
return {
|
|
295
|
+
contents: [
|
|
296
|
+
{
|
|
297
|
+
uri: uri.href,
|
|
298
|
+
mimeType: "text/plain",
|
|
299
|
+
text: `Failed to get policy: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
300
|
+
}
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/resources/index.ts
|
|
309
|
+
function registerResources(server, client) {
|
|
310
|
+
registerPolicyResources(server, client);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// src/server.ts
|
|
314
|
+
function createVettlyMcpServer(config) {
|
|
315
|
+
const client = new ModerationClient({
|
|
316
|
+
apiKey: config.apiKey,
|
|
317
|
+
apiUrl: config.apiUrl
|
|
318
|
+
});
|
|
319
|
+
const server = new McpServer({
|
|
320
|
+
name: "vettly",
|
|
321
|
+
version: "0.1.0"
|
|
322
|
+
});
|
|
323
|
+
registerTools(server, client);
|
|
324
|
+
registerResources(server, client);
|
|
325
|
+
return server;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export {
|
|
329
|
+
createVettlyMcpServer
|
|
330
|
+
};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createVettlyMcpServer: () => createVettlyMcpServer
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/server.ts
|
|
28
|
+
var import_mcp2 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
29
|
+
var import_sdk = require("@vettly/sdk");
|
|
30
|
+
|
|
31
|
+
// src/tools/moderate.ts
|
|
32
|
+
var import_zod = require("zod");
|
|
33
|
+
var ModerateInputSchema = import_zod.z.object({
|
|
34
|
+
content: import_zod.z.string().min(1).max(1e5).describe("The content to moderate (text content, or URL for images/video)"),
|
|
35
|
+
policyId: import_zod.z.string().min(1).describe("The policy ID to use for moderation"),
|
|
36
|
+
contentType: import_zod.z.enum(["text", "image", "video"]).default("text").describe("Type of content being moderated")
|
|
37
|
+
});
|
|
38
|
+
function registerModerateTool(server, client) {
|
|
39
|
+
server.tool(
|
|
40
|
+
"moderate_content",
|
|
41
|
+
"Check text, image, or video content against a Vettly moderation policy. Returns safety assessment with category scores.",
|
|
42
|
+
{
|
|
43
|
+
content: ModerateInputSchema.shape.content,
|
|
44
|
+
policyId: ModerateInputSchema.shape.policyId,
|
|
45
|
+
contentType: ModerateInputSchema.shape.contentType
|
|
46
|
+
},
|
|
47
|
+
async (args) => {
|
|
48
|
+
const input = ModerateInputSchema.parse(args);
|
|
49
|
+
try {
|
|
50
|
+
const result = await client.check({
|
|
51
|
+
content: input.content,
|
|
52
|
+
policyId: input.policyId,
|
|
53
|
+
contentType: input.contentType
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: JSON.stringify(
|
|
60
|
+
{
|
|
61
|
+
decisionId: result.decisionId,
|
|
62
|
+
safe: result.safe,
|
|
63
|
+
flagged: result.flagged,
|
|
64
|
+
action: result.action,
|
|
65
|
+
categories: result.categories,
|
|
66
|
+
provider: result.provider,
|
|
67
|
+
latency: result.latency,
|
|
68
|
+
cost: result.cost
|
|
69
|
+
},
|
|
70
|
+
null,
|
|
71
|
+
2
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: `Moderation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
82
|
+
}
|
|
83
|
+
],
|
|
84
|
+
isError: true
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/tools/validate.ts
|
|
92
|
+
var import_zod2 = require("zod");
|
|
93
|
+
var ValidateInputSchema = import_zod2.z.object({
|
|
94
|
+
yamlContent: import_zod2.z.string().min(1).describe("The YAML policy content to validate")
|
|
95
|
+
});
|
|
96
|
+
function registerValidateTool(server, client) {
|
|
97
|
+
server.tool(
|
|
98
|
+
"validate_policy",
|
|
99
|
+
"Validate a Vettly policy YAML without saving it. Returns validation results with any syntax or configuration errors.",
|
|
100
|
+
{
|
|
101
|
+
yamlContent: ValidateInputSchema.shape.yamlContent
|
|
102
|
+
},
|
|
103
|
+
async (args) => {
|
|
104
|
+
const input = ValidateInputSchema.parse(args);
|
|
105
|
+
try {
|
|
106
|
+
const result = await client.validatePolicy(input.yamlContent);
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: "text",
|
|
111
|
+
text: JSON.stringify(result, null, 2)
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
content: [
|
|
118
|
+
{
|
|
119
|
+
type: "text",
|
|
120
|
+
text: `Validation failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
isError: true
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/tools/list-policies.ts
|
|
131
|
+
function registerListPoliciesTool(server, client) {
|
|
132
|
+
server.tool(
|
|
133
|
+
"list_policies",
|
|
134
|
+
"List all moderation policies available in your Vettly account.",
|
|
135
|
+
{},
|
|
136
|
+
async () => {
|
|
137
|
+
try {
|
|
138
|
+
const result = await client.listPolicies();
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: "text",
|
|
143
|
+
text: JSON.stringify(result, null, 2)
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: "text",
|
|
152
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
isError: true
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/tools/usage-stats.ts
|
|
163
|
+
var import_zod3 = require("zod");
|
|
164
|
+
var UsageStatsInputSchema = import_zod3.z.object({
|
|
165
|
+
days: import_zod3.z.number().min(1).max(365).default(30).describe("Number of days to include in statistics")
|
|
166
|
+
});
|
|
167
|
+
function registerUsageStatsTool(server, client) {
|
|
168
|
+
server.tool(
|
|
169
|
+
"get_usage_stats",
|
|
170
|
+
"Get usage statistics for your Vettly account including request counts, costs, and moderation outcomes.",
|
|
171
|
+
{
|
|
172
|
+
days: UsageStatsInputSchema.shape.days
|
|
173
|
+
},
|
|
174
|
+
async (args) => {
|
|
175
|
+
const input = UsageStatsInputSchema.parse(args);
|
|
176
|
+
try {
|
|
177
|
+
const result = await client.getUsageStats(input.days);
|
|
178
|
+
return {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: "text",
|
|
182
|
+
text: JSON.stringify(result, null, 2)
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return {
|
|
188
|
+
content: [
|
|
189
|
+
{
|
|
190
|
+
type: "text",
|
|
191
|
+
text: `Failed to get usage stats: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
192
|
+
}
|
|
193
|
+
],
|
|
194
|
+
isError: true
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/tools/decisions.ts
|
|
202
|
+
var import_zod4 = require("zod");
|
|
203
|
+
var DecisionsInputSchema = import_zod4.z.object({
|
|
204
|
+
limit: import_zod4.z.number().min(1).max(50).default(10).describe("Number of decisions to return"),
|
|
205
|
+
flagged: import_zod4.z.boolean().optional().describe("Filter to only flagged content (true) or safe content (false)"),
|
|
206
|
+
policyId: import_zod4.z.string().optional().describe("Filter by specific policy ID"),
|
|
207
|
+
contentType: import_zod4.z.enum(["text", "image", "video"]).optional().describe("Filter by content type")
|
|
208
|
+
});
|
|
209
|
+
function registerDecisionsTool(server, client) {
|
|
210
|
+
server.tool(
|
|
211
|
+
"get_recent_decisions",
|
|
212
|
+
"Get recent moderation decisions with optional filtering by outcome, content type, or policy.",
|
|
213
|
+
{
|
|
214
|
+
limit: DecisionsInputSchema.shape.limit,
|
|
215
|
+
flagged: DecisionsInputSchema.shape.flagged,
|
|
216
|
+
policyId: DecisionsInputSchema.shape.policyId,
|
|
217
|
+
contentType: DecisionsInputSchema.shape.contentType
|
|
218
|
+
},
|
|
219
|
+
async (args) => {
|
|
220
|
+
const input = DecisionsInputSchema.parse(args);
|
|
221
|
+
try {
|
|
222
|
+
const result = await client.listDecisions({ limit: input.limit });
|
|
223
|
+
let decisions = result.decisions || result;
|
|
224
|
+
if (input.flagged !== void 0) {
|
|
225
|
+
decisions = decisions.filter((d) => d.flagged === input.flagged);
|
|
226
|
+
}
|
|
227
|
+
if (input.policyId) {
|
|
228
|
+
decisions = decisions.filter((d) => d.policyId === input.policyId);
|
|
229
|
+
}
|
|
230
|
+
if (input.contentType) {
|
|
231
|
+
decisions = decisions.filter((d) => d.contentType === input.contentType);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: "text",
|
|
237
|
+
text: JSON.stringify({ decisions }, null, 2)
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Failed to get decisions: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
247
|
+
}
|
|
248
|
+
],
|
|
249
|
+
isError: true
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// src/tools/index.ts
|
|
257
|
+
function registerTools(server, client) {
|
|
258
|
+
registerModerateTool(server, client);
|
|
259
|
+
registerValidateTool(server, client);
|
|
260
|
+
registerListPoliciesTool(server, client);
|
|
261
|
+
registerUsageStatsTool(server, client);
|
|
262
|
+
registerDecisionsTool(server, client);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/resources/policies.ts
|
|
266
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
267
|
+
function registerPolicyResources(server, client) {
|
|
268
|
+
server.resource(
|
|
269
|
+
"policies",
|
|
270
|
+
"vettly://policies",
|
|
271
|
+
{
|
|
272
|
+
description: "List of all available moderation policies",
|
|
273
|
+
mimeType: "application/json"
|
|
274
|
+
},
|
|
275
|
+
async (uri) => {
|
|
276
|
+
try {
|
|
277
|
+
const result = await client.listPolicies();
|
|
278
|
+
return {
|
|
279
|
+
contents: [
|
|
280
|
+
{
|
|
281
|
+
uri: uri.href,
|
|
282
|
+
mimeType: "application/json",
|
|
283
|
+
text: JSON.stringify(result, null, 2)
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
};
|
|
287
|
+
} catch (error) {
|
|
288
|
+
return {
|
|
289
|
+
contents: [
|
|
290
|
+
{
|
|
291
|
+
uri: uri.href,
|
|
292
|
+
mimeType: "text/plain",
|
|
293
|
+
text: `Failed to list policies: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
server.resource(
|
|
301
|
+
"policy",
|
|
302
|
+
new import_mcp.ResourceTemplate("vettly://policies/{policyId}", { list: void 0 }),
|
|
303
|
+
{
|
|
304
|
+
description: "A specific moderation policy with its YAML configuration",
|
|
305
|
+
mimeType: "application/x-yaml"
|
|
306
|
+
},
|
|
307
|
+
async (uri, { policyId }) => {
|
|
308
|
+
try {
|
|
309
|
+
const result = await client.getPolicy(policyId);
|
|
310
|
+
return {
|
|
311
|
+
contents: [
|
|
312
|
+
{
|
|
313
|
+
uri: uri.href,
|
|
314
|
+
mimeType: "application/x-yaml",
|
|
315
|
+
text: result.yamlContent || result.yaml || JSON.stringify(result, null, 2)
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
return {
|
|
321
|
+
contents: [
|
|
322
|
+
{
|
|
323
|
+
uri: uri.href,
|
|
324
|
+
mimeType: "text/plain",
|
|
325
|
+
text: `Failed to get policy: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/resources/index.ts
|
|
335
|
+
function registerResources(server, client) {
|
|
336
|
+
registerPolicyResources(server, client);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/server.ts
|
|
340
|
+
function createVettlyMcpServer(config) {
|
|
341
|
+
const client = new import_sdk.ModerationClient({
|
|
342
|
+
apiKey: config.apiKey,
|
|
343
|
+
apiUrl: config.apiUrl
|
|
344
|
+
});
|
|
345
|
+
const server = new import_mcp2.McpServer({
|
|
346
|
+
name: "vettly",
|
|
347
|
+
version: "0.1.0"
|
|
348
|
+
});
|
|
349
|
+
registerTools(server, client);
|
|
350
|
+
registerResources(server, client);
|
|
351
|
+
return server;
|
|
352
|
+
}
|
|
353
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
354
|
+
0 && (module.exports = {
|
|
355
|
+
createVettlyMcpServer
|
|
356
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
interface VettlyMcpConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
declare function createVettlyMcpServer(config: VettlyMcpConfig): McpServer;
|
|
8
|
+
|
|
9
|
+
export { type VettlyMcpConfig, createVettlyMcpServer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
|
|
3
|
+
interface VettlyMcpConfig {
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
declare function createVettlyMcpServer(config: VettlyMcpConfig): McpServer;
|
|
8
|
+
|
|
9
|
+
export { type VettlyMcpConfig, createVettlyMcpServer };
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vettly/mcp",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"mcpName": "io.github.code-with-brian/vettly",
|
|
5
|
+
"description": "MCP server for Vettly content moderation API",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"vettly-mcp": "./dist/bin/vettly-mcp.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsup src/index.ts src/bin/vettly-mcp.ts --format esm,cjs --dts --clean",
|
|
25
|
+
"dev": "tsup src/index.ts src/bin/vettly-mcp.ts --format esm --watch",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"prepublishOnly": "bun run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"mcp",
|
|
31
|
+
"mcp-server",
|
|
32
|
+
"model-context-protocol",
|
|
33
|
+
"vettly",
|
|
34
|
+
"content-moderation",
|
|
35
|
+
"moderation",
|
|
36
|
+
"content-safety",
|
|
37
|
+
"trust-safety",
|
|
38
|
+
"ai",
|
|
39
|
+
"claude",
|
|
40
|
+
"cursor",
|
|
41
|
+
"anthropic",
|
|
42
|
+
"ugc-moderation",
|
|
43
|
+
"text-moderation",
|
|
44
|
+
"image-moderation",
|
|
45
|
+
"video-moderation",
|
|
46
|
+
"hate-speech-detection",
|
|
47
|
+
"nsfw-detection",
|
|
48
|
+
"csam-detection",
|
|
49
|
+
"spam-detection"
|
|
50
|
+
],
|
|
51
|
+
"author": "Vettly",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/brian-nextaura/vettly-docs.git",
|
|
56
|
+
"directory": "packages/mcp"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://vettly.dev",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"name": "@nextauralabs/vettly-mcp"
|
|
62
|
+
},
|
|
63
|
+
"bugs": {
|
|
64
|
+
"url": "https://github.com/brian-nextaura/vettly-docs/issues"
|
|
65
|
+
},
|
|
66
|
+
"dependencies": {
|
|
67
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
68
|
+
"@vettly/sdk": "workspace:*",
|
|
69
|
+
"zod": "^3.22.4"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@types/node": "^20",
|
|
73
|
+
"tsup": "^8.5.1",
|
|
74
|
+
"typescript": "^5.9.3"
|
|
75
|
+
}
|
|
76
|
+
}
|