@tuteliq/mcp 3.2.0 → 3.2.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/dist/server.js +38 -18
- package/dist/src/formatters.js +11 -17
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +21 -24
- package/dist/src/tools/admin.js +38 -41
- package/dist/src/tools/analysis.js +32 -35
- package/dist/src/tools/detection.js +30 -33
- package/dist/src/tools/fraud.js +11 -14
- package/dist/src/tools/media.js +28 -31
- package/dist/src/transport.d.ts +6 -1
- package/dist/src/transport.d.ts.map +1 -1
- package/dist/src/transport.js +132 -32
- package/package.json +2 -1
package/dist/server.js
CHANGED
|
@@ -1,29 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
const express_1 = __importDefault(require("express"));
|
|
7
|
-
const cors_1 = __importDefault(require("cors"));
|
|
8
|
-
const index_js_1 = require("./src/index.js");
|
|
9
|
-
const transport_js_1 = require("./src/transport.js");
|
|
10
|
-
const app = (0, express_1.default)();
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import cors from 'cors';
|
|
3
|
+
import { createServer } from './src/index.js';
|
|
4
|
+
import { createHttpHandlers } from './src/transport.js';
|
|
5
|
+
const app = express();
|
|
11
6
|
const port = parseInt(process.env.PORT || '3001', 10);
|
|
12
|
-
app.use((
|
|
13
|
-
app.use(
|
|
14
|
-
const
|
|
15
|
-
|
|
7
|
+
app.use(cors());
|
|
8
|
+
app.use(express.json());
|
|
9
|
+
const { streamableHandler, sseHandler, messagesHandler, closeAll } = createHttpHandlers(createServer);
|
|
10
|
+
// Streamable HTTP transport (protocol version 2025-11-25)
|
|
16
11
|
app.all('/mcp', (req, res) => {
|
|
17
|
-
|
|
12
|
+
streamableHandler(req, res).catch((err) => {
|
|
18
13
|
console.error('MCP handler error:', err);
|
|
19
14
|
if (!res.headersSent) {
|
|
20
15
|
res.status(500).json({ error: 'Internal server error' });
|
|
21
16
|
}
|
|
22
17
|
});
|
|
23
18
|
});
|
|
19
|
+
// Legacy SSE transport (protocol version 2024-11-05)
|
|
20
|
+
app.get('/sse', (req, res) => {
|
|
21
|
+
sseHandler(req, res).catch((err) => {
|
|
22
|
+
console.error('SSE handler error:', err);
|
|
23
|
+
if (!res.headersSent) {
|
|
24
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
app.post('/messages', (req, res) => {
|
|
29
|
+
messagesHandler(req, res).catch((err) => {
|
|
30
|
+
console.error('Messages handler error:', err);
|
|
31
|
+
if (!res.headersSent) {
|
|
32
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
24
36
|
app.get('/health', (_req, res) => {
|
|
25
|
-
res.json({ status: 'ok', version: '3.
|
|
37
|
+
res.json({ status: 'ok', version: '3.2.0' });
|
|
38
|
+
});
|
|
39
|
+
const server = app.listen(port, () => {
|
|
40
|
+
console.error(`Tuteliq MCP server running on http://localhost:${port}`);
|
|
41
|
+
console.error(` Streamable HTTP: POST/GET/DELETE /mcp`);
|
|
42
|
+
console.error(` Legacy SSE: GET /sse + POST /messages`);
|
|
26
43
|
});
|
|
27
|
-
|
|
28
|
-
console.error(
|
|
44
|
+
process.on('SIGINT', async () => {
|
|
45
|
+
console.error('Shutting down...');
|
|
46
|
+
await closeAll();
|
|
47
|
+
server.close();
|
|
48
|
+
process.exit(0);
|
|
29
49
|
});
|
package/dist/src/formatters.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.trendEmoji = exports.riskEmoji = exports.severityEmoji = void 0;
|
|
4
|
-
exports.formatDetectionResult = formatDetectionResult;
|
|
5
|
-
exports.formatMultiResult = formatMultiResult;
|
|
6
|
-
exports.formatVideoResult = formatVideoResult;
|
|
7
|
-
exports.severityEmoji = {
|
|
1
|
+
export const severityEmoji = {
|
|
8
2
|
low: '\u{1F7E1}',
|
|
9
3
|
medium: '\u{1F7E0}',
|
|
10
4
|
high: '\u{1F534}',
|
|
11
5
|
critical: '\u26D4',
|
|
12
6
|
};
|
|
13
|
-
|
|
7
|
+
export const riskEmoji = {
|
|
14
8
|
safe: '\u2705',
|
|
15
9
|
none: '\u2705',
|
|
16
10
|
low: '\u{1F7E1}',
|
|
@@ -18,14 +12,14 @@ exports.riskEmoji = {
|
|
|
18
12
|
high: '\u{1F534}',
|
|
19
13
|
critical: '\u26D4',
|
|
20
14
|
};
|
|
21
|
-
|
|
15
|
+
export const trendEmoji = {
|
|
22
16
|
improving: '\u{1F4C8}',
|
|
23
17
|
stable: '\u27A1\uFE0F',
|
|
24
18
|
worsening: '\u{1F4C9}',
|
|
25
19
|
};
|
|
26
|
-
function formatDetectionResult(result) {
|
|
20
|
+
export function formatDetectionResult(result) {
|
|
27
21
|
const detected = result.detected;
|
|
28
|
-
const levelEmoji =
|
|
22
|
+
const levelEmoji = riskEmoji[result.level] || '\u26AA';
|
|
29
23
|
const label = result.endpoint
|
|
30
24
|
.split('_')
|
|
31
25
|
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
|
|
@@ -62,9 +56,9 @@ ${evidence}
|
|
|
62
56
|
${messageAnalysis}
|
|
63
57
|
${calibration}`.trim();
|
|
64
58
|
}
|
|
65
|
-
function formatMultiResult(result) {
|
|
59
|
+
export function formatMultiResult(result) {
|
|
66
60
|
const s = result.summary;
|
|
67
|
-
const overallEmoji =
|
|
61
|
+
const overallEmoji = riskEmoji[s.overall_risk_level] || '\u26AA';
|
|
68
62
|
const summarySection = `## Multi-Endpoint Analysis
|
|
69
63
|
|
|
70
64
|
**Overall Risk:** ${overallEmoji} ${s.overall_risk_level}
|
|
@@ -74,7 +68,7 @@ function formatMultiResult(result) {
|
|
|
74
68
|
${result.cross_endpoint_modifier ? `**Cross-Endpoint Modifier:** ${result.cross_endpoint_modifier.toFixed(2)}x` : ''}`;
|
|
75
69
|
const perEndpoint = result.results
|
|
76
70
|
.map(r => {
|
|
77
|
-
const emoji = r.detected ? (
|
|
71
|
+
const emoji = r.detected ? (riskEmoji[r.level] || '\u26AA') : '\u2705';
|
|
78
72
|
return `### ${emoji} ${r.endpoint}
|
|
79
73
|
**Detected:** ${r.detected ? 'Yes' : 'No'} | **Risk:** ${(r.risk_score * 100).toFixed(0)}% | **Level:** ${r.level}
|
|
80
74
|
${r.categories.length > 0 ? `**Categories:** ${r.categories.map(c => c.tag).join(', ')}` : ''}
|
|
@@ -87,12 +81,12 @@ ${r.rationale}`;
|
|
|
87
81
|
|
|
88
82
|
${perEndpoint}`;
|
|
89
83
|
}
|
|
90
|
-
function formatVideoResult(result) {
|
|
91
|
-
const emoji =
|
|
84
|
+
export function formatVideoResult(result) {
|
|
85
|
+
const emoji = severityEmoji[result.overall_severity] || '\u2705';
|
|
92
86
|
const findingsSection = result.safety_findings.length > 0
|
|
93
87
|
? result.safety_findings
|
|
94
88
|
.map(f => {
|
|
95
|
-
const fEmoji =
|
|
89
|
+
const fEmoji = severityEmoji[f.severity <= 0.3 ? 'low' : f.severity <= 0.6 ? 'medium' : f.severity <= 0.85 ? 'high' : 'critical'] || '\u26AA';
|
|
96
90
|
return `- \`${f.timestamp.toFixed(1)}s\` (frame ${f.frame_index}) ${fEmoji} ${f.description}\n Categories: ${f.categories.join(', ')} | Severity: ${(f.severity * 100).toFixed(0)}%`;
|
|
97
91
|
})
|
|
98
92
|
.join('\n')
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUpE,wBAAgB,YAAY,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUpE,wBAAgB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAsBvD"}
|
package/dist/src/index.js
CHANGED
|
@@ -1,38 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
function createServer() {
|
|
14
|
-
const apiKey = process.env.TUTELIQ_API_KEY;
|
|
15
|
-
if (!apiKey) {
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { Tuteliq } from '@tuteliq/sdk';
|
|
4
|
+
import { registerDetectionTools } from './tools/detection.js';
|
|
5
|
+
import { registerFraudTools } from './tools/fraud.js';
|
|
6
|
+
import { registerMediaTools } from './tools/media.js';
|
|
7
|
+
import { registerAnalysisTools } from './tools/analysis.js';
|
|
8
|
+
import { registerAdminTools } from './tools/admin.js';
|
|
9
|
+
import { getTransportMode, startStdio } from './transport.js';
|
|
10
|
+
export function createServer(apiKey) {
|
|
11
|
+
const key = apiKey || process.env.TUTELIQ_API_KEY;
|
|
12
|
+
if (!key) {
|
|
16
13
|
console.error('Error: TUTELIQ_API_KEY environment variable is required');
|
|
17
14
|
process.exit(1);
|
|
18
15
|
}
|
|
19
|
-
const client = new
|
|
20
|
-
const server = new
|
|
16
|
+
const client = new Tuteliq(key);
|
|
17
|
+
const server = new McpServer({
|
|
21
18
|
name: 'tuteliq-mcp',
|
|
22
|
-
version: '3.
|
|
19
|
+
version: '3.2.0',
|
|
23
20
|
});
|
|
24
21
|
// Register all tool groups
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
registerDetectionTools(server, client);
|
|
23
|
+
registerFraudTools(server, client);
|
|
24
|
+
registerMediaTools(server, client);
|
|
25
|
+
registerAnalysisTools(server, client);
|
|
26
|
+
registerAdminTools(server, client);
|
|
30
27
|
return server;
|
|
31
28
|
}
|
|
32
29
|
// Direct execution: stdio mode
|
|
33
|
-
if (
|
|
30
|
+
if (getTransportMode() === 'stdio') {
|
|
34
31
|
const server = createServer();
|
|
35
|
-
|
|
32
|
+
startStdio(server).catch((error) => {
|
|
36
33
|
console.error('Fatal error:', error);
|
|
37
34
|
process.exit(1);
|
|
38
35
|
});
|
package/dist/src/tools/admin.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const formatters_js_1 = require("../formatters.js");
|
|
6
|
-
function registerAdminTools(server, client) {
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { severityEmoji } from '../formatters.js';
|
|
3
|
+
export function registerAdminTools(server, client) {
|
|
7
4
|
// =========================================================================
|
|
8
5
|
// Webhook Management
|
|
9
6
|
// =========================================================================
|
|
@@ -16,9 +13,9 @@ function registerAdminTools(server, client) {
|
|
|
16
13
|
return { content: [{ type: 'text', text: `## Webhooks\n\n${lines}` }] };
|
|
17
14
|
});
|
|
18
15
|
server.tool('create_webhook', 'Create a new webhook endpoint.', {
|
|
19
|
-
name:
|
|
20
|
-
url:
|
|
21
|
-
events:
|
|
16
|
+
name: z.string().describe('Display name for the webhook'),
|
|
17
|
+
url: z.string().describe('HTTPS URL to receive webhook payloads'),
|
|
18
|
+
events: z.array(z.string()).describe('Event types to subscribe to'),
|
|
22
19
|
}, async ({ name, url, events }) => {
|
|
23
20
|
const result = await client.createWebhook({
|
|
24
21
|
name,
|
|
@@ -28,11 +25,11 @@ function registerAdminTools(server, client) {
|
|
|
28
25
|
return { content: [{ type: 'text', text: `## \u2705 Webhook Created\n\n**ID:** ${result.id}\n**Name:** ${result.name}\n**URL:** ${result.url}\n**Events:** ${result.events.join(', ')}\n\n\u26A0\uFE0F **Secret (save this \u2014 shown only once):**\n\`${result.secret}\`` }] };
|
|
29
26
|
});
|
|
30
27
|
server.tool('update_webhook', 'Update an existing webhook configuration.', {
|
|
31
|
-
id:
|
|
32
|
-
name:
|
|
33
|
-
url:
|
|
34
|
-
events:
|
|
35
|
-
is_active:
|
|
28
|
+
id: z.string().describe('Webhook ID'),
|
|
29
|
+
name: z.string().optional().describe('New display name'),
|
|
30
|
+
url: z.string().optional().describe('New HTTPS URL'),
|
|
31
|
+
events: z.array(z.string()).optional().describe('New event subscriptions'),
|
|
32
|
+
is_active: z.boolean().optional().describe('Enable or disable the webhook'),
|
|
36
33
|
}, async ({ id, name, url, events, is_active }) => {
|
|
37
34
|
const result = await client.updateWebhook(id, {
|
|
38
35
|
name,
|
|
@@ -42,15 +39,15 @@ function registerAdminTools(server, client) {
|
|
|
42
39
|
});
|
|
43
40
|
return { content: [{ type: 'text', text: `## \u2705 Webhook Updated\n\n**ID:** ${result.id}\n**Name:** ${result.name}\n**Active:** ${result.is_active ? '\u{1F7E2} Yes' : '\u26AA No'}` }] };
|
|
44
41
|
});
|
|
45
|
-
server.tool('delete_webhook', 'Permanently delete a webhook.', { id:
|
|
42
|
+
server.tool('delete_webhook', 'Permanently delete a webhook.', { id: z.string().describe('Webhook ID to delete') }, async ({ id }) => {
|
|
46
43
|
await client.deleteWebhook(id);
|
|
47
44
|
return { content: [{ type: 'text', text: `## \u2705 Webhook Deleted\n\nWebhook \`${id}\` has been permanently deleted.` }] };
|
|
48
45
|
});
|
|
49
|
-
server.tool('test_webhook', 'Send a test payload to a webhook to verify it is working correctly.', { id:
|
|
46
|
+
server.tool('test_webhook', 'Send a test payload to a webhook to verify it is working correctly.', { id: z.string().describe('Webhook ID to test') }, async ({ id }) => {
|
|
50
47
|
const result = await client.testWebhook(id);
|
|
51
48
|
return { content: [{ type: 'text', text: `## ${result.success ? '\u2705' : '\u274C'} Webhook Test\n\n**Success:** ${result.success}\n**Status Code:** ${result.status_code}\n**Latency:** ${result.latency_ms}ms${result.error ? `\n**Error:** ${result.error}` : ''}` }] };
|
|
52
49
|
});
|
|
53
|
-
server.tool('regenerate_webhook_secret', 'Regenerate a webhook signing secret.', { id:
|
|
50
|
+
server.tool('regenerate_webhook_secret', 'Regenerate a webhook signing secret.', { id: z.string().describe('Webhook ID') }, async ({ id }) => {
|
|
54
51
|
const result = await client.regenerateWebhookSecret(id);
|
|
55
52
|
return { content: [{ type: 'text', text: `## \u2705 Secret Regenerated\n\nThe old secret has been invalidated.\n\n\u26A0\uFE0F **New Secret (save this \u2014 shown only once):**\n\`${result.secret}\`` }] };
|
|
56
53
|
});
|
|
@@ -70,7 +67,7 @@ function registerAdminTools(server, client) {
|
|
|
70
67
|
// =========================================================================
|
|
71
68
|
// Usage & Billing
|
|
72
69
|
// =========================================================================
|
|
73
|
-
server.tool('get_usage_history', 'Get daily usage history for the past N days.', { days:
|
|
70
|
+
server.tool('get_usage_history', 'Get daily usage history for the past N days.', { days: z.number().optional().describe('Number of days to retrieve (1-30, default: 7)') }, async ({ days }) => {
|
|
74
71
|
const result = await client.getUsageHistory(days);
|
|
75
72
|
if (result.days.length === 0) {
|
|
76
73
|
return { content: [{ type: 'text', text: 'No usage data available.' }] };
|
|
@@ -78,7 +75,7 @@ function registerAdminTools(server, client) {
|
|
|
78
75
|
const lines = result.days.map(d => `| ${d.date} | ${d.total_requests} | ${d.success_requests} | ${d.error_requests} |`).join('\n');
|
|
79
76
|
return { content: [{ type: 'text', text: `## Usage History\n\n| Date | Total | Success | Errors |\n|------|-------|---------|--------|\n${lines}` }] };
|
|
80
77
|
});
|
|
81
|
-
server.tool('get_usage_by_tool', 'Get usage broken down by tool/endpoint for a specific date.', { date:
|
|
78
|
+
server.tool('get_usage_by_tool', 'Get usage broken down by tool/endpoint for a specific date.', { date: z.string().optional().describe('Date in YYYY-MM-DD format (default: today)') }, async ({ date }) => {
|
|
82
79
|
const result = await client.getUsageByTool(date);
|
|
83
80
|
const toolLines = Object.entries(result.tools).map(([tool, count]) => `- **${tool}:** ${count}`).join('\n');
|
|
84
81
|
const endpointLines = Object.entries(result.endpoints).map(([ep, count]) => `- **${ep}:** ${count}`).join('\n');
|
|
@@ -111,10 +108,10 @@ ${result.recommendations ? `### Recommendation\n${result.recommendations.reason}
|
|
|
111
108
|
const collections = Object.keys(result.data).join(', ');
|
|
112
109
|
return { content: [{ type: 'text', text: `## \u{1F4E6} Account Data Export\n\n**User ID:** ${result.userId}\n**Exported At:** ${result.exportedAt}\n**Collections:** ${collections}\n\n\`\`\`json\n${JSON.stringify(result.data, null, 2).slice(0, 5000)}\n\`\`\`` }] };
|
|
113
110
|
});
|
|
114
|
-
const consentTypeEnum =
|
|
111
|
+
const consentTypeEnum = z.enum(['data_processing', 'analytics', 'marketing', 'third_party_sharing', 'child_safety_monitoring']);
|
|
115
112
|
server.tool('record_consent', 'Record user consent for data processing (GDPR Article 7).', {
|
|
116
113
|
consent_type: consentTypeEnum.describe('Type of consent to record'),
|
|
117
|
-
version:
|
|
114
|
+
version: z.string().describe('Policy version the user is consenting to'),
|
|
118
115
|
}, async ({ consent_type, version }) => {
|
|
119
116
|
const result = await client.recordConsent({ consent_type: consent_type, version });
|
|
120
117
|
return { content: [{ type: 'text', text: `## \u2705 Consent Recorded\n\n**Type:** ${result.consent.consent_type}\n**Status:** ${result.consent.status}\n**Version:** ${result.consent.version}` }] };
|
|
@@ -132,16 +129,16 @@ ${result.recommendations ? `### Recommendation\n${result.recommendations.reason}
|
|
|
132
129
|
return { content: [{ type: 'text', text: `## \u26A0\uFE0F Consent Withdrawn\n\n**Type:** ${result.consent.consent_type}\n**Status:** ${result.consent.status}` }] };
|
|
133
130
|
});
|
|
134
131
|
server.tool('rectify_data', 'Rectify (correct) user data (GDPR Right to Rectification).', {
|
|
135
|
-
collection:
|
|
136
|
-
document_id:
|
|
137
|
-
fields:
|
|
132
|
+
collection: z.string().describe('Firestore collection name'),
|
|
133
|
+
document_id: z.string().describe('Document ID to rectify'),
|
|
134
|
+
fields: z.record(z.string(), z.unknown()).describe('Fields to update'),
|
|
138
135
|
}, async ({ collection, document_id, fields }) => {
|
|
139
136
|
const result = await client.rectifyData({ collection, document_id, fields: fields });
|
|
140
137
|
return { content: [{ type: 'text', text: `## \u2705 Data Rectified\n\n**Message:** ${result.message}\n**Updated Fields:** ${result.updated_fields.join(', ')}` }] };
|
|
141
138
|
});
|
|
142
139
|
server.tool('get_audit_logs', 'Get audit trail of data operations.', {
|
|
143
|
-
action:
|
|
144
|
-
limit:
|
|
140
|
+
action: z.enum(['data_access', 'data_export', 'data_deletion', 'data_rectification', 'consent_granted', 'consent_withdrawn', 'breach_notification']).optional().describe('Filter by action type'),
|
|
141
|
+
limit: z.number().optional().describe('Maximum number of results'),
|
|
145
142
|
}, async ({ action, limit }) => {
|
|
146
143
|
const result = await client.getAuditLogs({ action: action, limit });
|
|
147
144
|
if (result.audit_logs.length === 0) {
|
|
@@ -154,12 +151,12 @@ ${result.recommendations ? `### Recommendation\n${result.recommendations.reason}
|
|
|
154
151
|
// Breach Management
|
|
155
152
|
// =========================================================================
|
|
156
153
|
server.tool('log_breach', 'Log a new data breach (GDPR Article 33/34).', {
|
|
157
|
-
title:
|
|
158
|
-
description:
|
|
159
|
-
severity:
|
|
160
|
-
affected_user_ids:
|
|
161
|
-
data_categories:
|
|
162
|
-
reported_by:
|
|
154
|
+
title: z.string().describe('Brief title of the breach'),
|
|
155
|
+
description: z.string().describe('Detailed description'),
|
|
156
|
+
severity: z.enum(['low', 'medium', 'high', 'critical']).describe('Breach severity'),
|
|
157
|
+
affected_user_ids: z.array(z.string()).describe('List of affected user IDs'),
|
|
158
|
+
data_categories: z.array(z.string()).describe('Categories of data affected'),
|
|
159
|
+
reported_by: z.string().describe('Who reported the breach'),
|
|
163
160
|
}, async ({ title, description, severity, affected_user_ids, data_categories, reported_by }) => {
|
|
164
161
|
const result = await client.logBreach({
|
|
165
162
|
title,
|
|
@@ -170,30 +167,30 @@ ${result.recommendations ? `### Recommendation\n${result.recommendations.reason}
|
|
|
170
167
|
reported_by,
|
|
171
168
|
});
|
|
172
169
|
const b = result.breach;
|
|
173
|
-
return { content: [{ type: 'text', text: `## \u26A0\uFE0F Breach Logged\n\n**ID:** ${b.id}\n**Title:** ${b.title}\n**Severity:** ${
|
|
170
|
+
return { content: [{ type: 'text', text: `## \u26A0\uFE0F Breach Logged\n\n**ID:** ${b.id}\n**Title:** ${b.title}\n**Severity:** ${severityEmoji[b.severity] || '\u26AA'} ${b.severity}\n**Status:** ${b.status}\n**Notification Deadline:** ${b.notification_deadline}\n**Affected Users:** ${b.affected_user_ids.length}\n**Data Categories:** ${b.data_categories.join(', ')}` }] };
|
|
174
171
|
});
|
|
175
|
-
const breachStatusEnum =
|
|
172
|
+
const breachStatusEnum = z.enum(['detected', 'investigating', 'contained', 'reported', 'resolved']);
|
|
176
173
|
server.tool('list_breaches', 'List all data breaches.', {
|
|
177
174
|
status: breachStatusEnum.optional().describe('Filter by status'),
|
|
178
|
-
limit:
|
|
175
|
+
limit: z.number().optional().describe('Maximum number of results'),
|
|
179
176
|
}, async ({ status, limit }) => {
|
|
180
177
|
const result = await client.listBreaches({ status: status, limit });
|
|
181
178
|
if (result.breaches.length === 0) {
|
|
182
179
|
return { content: [{ type: 'text', text: 'No breaches found.' }] };
|
|
183
180
|
}
|
|
184
|
-
const breachLines = result.breaches.map(b => `- ${
|
|
181
|
+
const breachLines = result.breaches.map(b => `- ${severityEmoji[b.severity] || '\u26AA'} **${b.title}** \u2014 ${b.status} _(${b.id})_`).join('\n');
|
|
185
182
|
return { content: [{ type: 'text', text: `## Data Breaches\n\n${breachLines}` }] };
|
|
186
183
|
});
|
|
187
|
-
server.tool('get_breach', 'Get details of a specific data breach.', { id:
|
|
184
|
+
server.tool('get_breach', 'Get details of a specific data breach.', { id: z.string().describe('Breach ID') }, async ({ id }) => {
|
|
188
185
|
const result = await client.getBreach(id);
|
|
189
186
|
const b = result.breach;
|
|
190
|
-
return { content: [{ type: 'text', text: `## Breach Details\n\n**ID:** ${b.id}\n**Title:** ${b.title}\n**Severity:** ${
|
|
187
|
+
return { content: [{ type: 'text', text: `## Breach Details\n\n**ID:** ${b.id}\n**Title:** ${b.title}\n**Severity:** ${severityEmoji[b.severity] || '\u26AA'} ${b.severity}\n**Status:** ${b.status}\n**Notification:** ${b.notification_status}\n**Reported By:** ${b.reported_by}\n**Deadline:** ${b.notification_deadline}\n**Created:** ${b.created_at}\n**Updated:** ${b.updated_at}\n\n### Description\n${b.description}\n\n**Affected Users:** ${b.affected_user_ids.join(', ')}\n**Data Categories:** ${b.data_categories.join(', ')}` }] };
|
|
191
188
|
});
|
|
192
189
|
server.tool('update_breach_status', 'Update a breach status and notification progress.', {
|
|
193
|
-
id:
|
|
190
|
+
id: z.string().describe('Breach ID'),
|
|
194
191
|
status: breachStatusEnum.describe('New breach status'),
|
|
195
|
-
notification_status:
|
|
196
|
-
notes:
|
|
192
|
+
notification_status: z.enum(['pending', 'users_notified', 'dpa_notified', 'completed']).optional().describe('Notification progress'),
|
|
193
|
+
notes: z.string().optional().describe('Additional notes'),
|
|
197
194
|
}, async ({ id, status, notification_status, notes }) => {
|
|
198
195
|
const result = await client.updateBreachStatus(id, {
|
|
199
196
|
status: status,
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const fs_1 = require("fs");
|
|
7
|
-
const path_1 = require("path");
|
|
8
|
-
const formatters_js_1 = require("../formatters.js");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { trendEmoji, riskEmoji, formatMultiResult } from '../formatters.js';
|
|
9
6
|
const EMOTIONS_WIDGET_URI = 'ui://tuteliq/emotions-result.html';
|
|
10
7
|
const ACTION_PLAN_WIDGET_URI = 'ui://tuteliq/action-plan.html';
|
|
11
8
|
const REPORT_WIDGET_URI = 'ui://tuteliq/report-result.html';
|
|
12
9
|
const MULTI_WIDGET_URI = 'ui://tuteliq/multi-result.html';
|
|
13
10
|
function loadWidget(name) {
|
|
14
|
-
return
|
|
11
|
+
return readFileSync(resolve(__dirname, '../../dist-ui', name), 'utf-8');
|
|
15
12
|
}
|
|
16
|
-
function registerAnalysisTools(server, client) {
|
|
13
|
+
export function registerAnalysisTools(server, client) {
|
|
17
14
|
const resources = [
|
|
18
15
|
{ uri: EMOTIONS_WIDGET_URI, file: 'emotions-result.html' },
|
|
19
16
|
{ uri: ACTION_PLAN_WIDGET_URI, file: 'action-plan.html' },
|
|
@@ -21,21 +18,21 @@ function registerAnalysisTools(server, client) {
|
|
|
21
18
|
{ uri: MULTI_WIDGET_URI, file: 'multi-result.html' },
|
|
22
19
|
];
|
|
23
20
|
for (const r of resources) {
|
|
24
|
-
|
|
21
|
+
registerAppResource(server, r.uri, r.uri, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
|
|
25
22
|
contents: [{
|
|
26
23
|
uri: r.uri,
|
|
27
|
-
mimeType:
|
|
24
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
28
25
|
text: loadWidget(r.file),
|
|
29
26
|
}],
|
|
30
27
|
}));
|
|
31
28
|
}
|
|
32
29
|
// ── analyze_emotions ───────────────────────────────────────────────────────
|
|
33
|
-
|
|
30
|
+
registerAppTool(server, 'analyze_emotions', {
|
|
34
31
|
title: 'Analyze Emotions',
|
|
35
32
|
description: 'Analyze emotional content and mental state indicators. Identifies dominant emotions, trends, and provides follow-up recommendations.',
|
|
36
33
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
37
34
|
inputSchema: {
|
|
38
|
-
content:
|
|
35
|
+
content: z.string().describe('The text content to analyze for emotions'),
|
|
39
36
|
},
|
|
40
37
|
_meta: {
|
|
41
38
|
ui: { resourceUri: EMOTIONS_WIDGET_URI },
|
|
@@ -45,7 +42,7 @@ function registerAnalysisTools(server, client) {
|
|
|
45
42
|
},
|
|
46
43
|
}, async ({ content }) => {
|
|
47
44
|
const result = await client.analyzeEmotions({ content });
|
|
48
|
-
const emoji =
|
|
45
|
+
const emoji = trendEmoji[result.trend] || '\u27A1\uFE0F';
|
|
49
46
|
const emotionScoresList = Object.entries(result.emotion_scores)
|
|
50
47
|
.sort((a, b) => b[1] - a[1])
|
|
51
48
|
.map(([emotion, score]) => `- ${emotion}: ${(score * 100).toFixed(0)}%`)
|
|
@@ -69,15 +66,15 @@ ${result.recommended_followup}`;
|
|
|
69
66
|
};
|
|
70
67
|
});
|
|
71
68
|
// ── get_action_plan ────────────────────────────────────────────────────────
|
|
72
|
-
|
|
69
|
+
registerAppTool(server, 'get_action_plan', {
|
|
73
70
|
title: 'Get Action Plan',
|
|
74
71
|
description: 'Generate age-appropriate guidance and action steps for handling a safety situation.',
|
|
75
72
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
76
73
|
inputSchema: {
|
|
77
|
-
situation:
|
|
78
|
-
childAge:
|
|
79
|
-
audience:
|
|
80
|
-
severity:
|
|
74
|
+
situation: z.string().describe('Description of the situation needing guidance'),
|
|
75
|
+
childAge: z.number().optional().describe('Age of the child involved'),
|
|
76
|
+
audience: z.enum(['child', 'parent', 'educator', 'platform']).optional().describe('Who the guidance is for (default: parent)'),
|
|
77
|
+
severity: z.enum(['low', 'medium', 'high', 'critical']).optional().describe('Severity of the situation'),
|
|
81
78
|
},
|
|
82
79
|
_meta: {
|
|
83
80
|
ui: { resourceUri: ACTION_PLAN_WIDGET_URI },
|
|
@@ -101,17 +98,17 @@ ${result.steps.map((step, i) => `${i + 1}. ${step}`).join('\n')}`;
|
|
|
101
98
|
};
|
|
102
99
|
});
|
|
103
100
|
// ── generate_report ────────────────────────────────────────────────────────
|
|
104
|
-
|
|
101
|
+
registerAppTool(server, 'generate_report', {
|
|
105
102
|
title: 'Generate Report',
|
|
106
103
|
description: 'Generate a comprehensive incident report from a conversation.',
|
|
107
104
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
108
105
|
inputSchema: {
|
|
109
|
-
messages:
|
|
110
|
-
sender:
|
|
111
|
-
content:
|
|
106
|
+
messages: z.array(z.object({
|
|
107
|
+
sender: z.string().describe('Name/ID of sender'),
|
|
108
|
+
content: z.string().describe('Message content'),
|
|
112
109
|
})).describe('Array of messages in the incident'),
|
|
113
|
-
childAge:
|
|
114
|
-
incidentType:
|
|
110
|
+
childAge: z.number().optional().describe('Age of the child involved'),
|
|
111
|
+
incidentType: z.string().optional().describe('Type of incident (e.g., bullying, grooming)'),
|
|
115
112
|
},
|
|
116
113
|
_meta: {
|
|
117
114
|
ui: { resourceUri: REPORT_WIDGET_URI },
|
|
@@ -125,7 +122,7 @@ ${result.steps.map((step, i) => `${i + 1}. ${step}`).join('\n')}`;
|
|
|
125
122
|
childAge,
|
|
126
123
|
incident: incidentType ? { type: incidentType } : undefined,
|
|
127
124
|
});
|
|
128
|
-
const emoji =
|
|
125
|
+
const emoji = riskEmoji[result.risk_level] || '\u26AA';
|
|
129
126
|
const text = `## \u{1F4CB} Incident Report
|
|
130
127
|
|
|
131
128
|
**Risk Level:** ${emoji} ${result.risk_level.charAt(0).toUpperCase() + result.risk_level.slice(1)}
|
|
@@ -144,17 +141,17 @@ ${result.recommended_next_steps.map((step, i) => `${i + 1}. ${step}`).join('\n')
|
|
|
144
141
|
};
|
|
145
142
|
});
|
|
146
143
|
// ── analyse_multi ──────────────────────────────────────────────────────────
|
|
147
|
-
|
|
144
|
+
registerAppTool(server, 'analyse_multi', {
|
|
148
145
|
title: 'Multi-Endpoint Analysis',
|
|
149
146
|
description: 'Run multiple detection endpoints on a single piece of text.',
|
|
150
147
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
151
148
|
inputSchema: {
|
|
152
|
-
content:
|
|
153
|
-
endpoints:
|
|
154
|
-
context:
|
|
155
|
-
include_evidence:
|
|
156
|
-
external_id:
|
|
157
|
-
customer_id:
|
|
149
|
+
content: z.string().describe('Text content to analyze'),
|
|
150
|
+
endpoints: z.array(z.string()).describe('Detection endpoints to run'),
|
|
151
|
+
context: z.record(z.string(), z.unknown()).optional().describe('Optional analysis context'),
|
|
152
|
+
include_evidence: z.boolean().optional().describe('Include supporting evidence'),
|
|
153
|
+
external_id: z.string().optional().describe('External tracking ID'),
|
|
154
|
+
customer_id: z.string().optional().describe('Customer identifier'),
|
|
158
155
|
},
|
|
159
156
|
_meta: {
|
|
160
157
|
ui: { resourceUri: MULTI_WIDGET_URI },
|
|
@@ -173,7 +170,7 @@ ${result.recommended_next_steps.map((step, i) => `${i + 1}. ${step}`).join('\n')
|
|
|
173
170
|
});
|
|
174
171
|
return {
|
|
175
172
|
structuredContent: { toolName: 'analyse_multi', result, branding: { appName: 'Tuteliq' } },
|
|
176
|
-
content: [{ type: 'text', text:
|
|
173
|
+
content: [{ type: 'text', text: formatMultiResult(result) }],
|
|
177
174
|
};
|
|
178
175
|
});
|
|
179
176
|
}
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const fs_1 = require("fs");
|
|
7
|
-
const path_1 = require("path");
|
|
8
|
-
const formatters_js_1 = require("../formatters.js");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { severityEmoji, riskEmoji } from '../formatters.js';
|
|
9
6
|
const DETECTION_WIDGET_URI = 'ui://tuteliq/detection-result.html';
|
|
10
7
|
function loadWidget(name) {
|
|
11
|
-
return
|
|
8
|
+
return readFileSync(resolve(__dirname, '../../dist-ui', name), 'utf-8');
|
|
12
9
|
}
|
|
13
|
-
const contextSchema =
|
|
14
|
-
language:
|
|
15
|
-
ageGroup:
|
|
16
|
-
relationship:
|
|
17
|
-
platform:
|
|
10
|
+
const contextSchema = z.object({
|
|
11
|
+
language: z.string().optional(),
|
|
12
|
+
ageGroup: z.string().optional(),
|
|
13
|
+
relationship: z.string().optional(),
|
|
14
|
+
platform: z.string().optional(),
|
|
18
15
|
}).optional();
|
|
19
16
|
const uiMeta = (desc, invoking, invoked) => ({
|
|
20
17
|
ui: { resourceUri: DETECTION_WIDGET_URI },
|
|
@@ -22,22 +19,22 @@ const uiMeta = (desc, invoking, invoked) => ({
|
|
|
22
19
|
'openai/toolInvocation/invoking': invoking,
|
|
23
20
|
'openai/toolInvocation/invoked': invoked,
|
|
24
21
|
});
|
|
25
|
-
function registerDetectionTools(server, client) {
|
|
22
|
+
export function registerDetectionTools(server, client) {
|
|
26
23
|
// Cast needed due to CJS/ESM dual-module type mismatch between MCP SDK and ext-apps
|
|
27
|
-
|
|
24
|
+
registerAppResource(server, DETECTION_WIDGET_URI, DETECTION_WIDGET_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
|
|
28
25
|
contents: [{
|
|
29
26
|
uri: DETECTION_WIDGET_URI,
|
|
30
|
-
mimeType:
|
|
27
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
31
28
|
text: loadWidget('detection-result.html'),
|
|
32
29
|
}],
|
|
33
30
|
}));
|
|
34
31
|
// ── detect_bullying ────────────────────────────────────────────────────────
|
|
35
|
-
|
|
32
|
+
registerAppTool(server, 'detect_bullying', {
|
|
36
33
|
title: 'Detect Bullying',
|
|
37
34
|
description: 'Analyze text content to detect bullying, harassment, or harmful language.',
|
|
38
35
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
39
36
|
inputSchema: {
|
|
40
|
-
content:
|
|
37
|
+
content: z.string().describe('The text content to analyze for bullying'),
|
|
41
38
|
context: contextSchema,
|
|
42
39
|
},
|
|
43
40
|
_meta: uiMeta('Shows bullying detection results with risk indicators', 'Analyzing content for bullying...', 'Bullying analysis complete.'),
|
|
@@ -46,7 +43,7 @@ function registerDetectionTools(server, client) {
|
|
|
46
43
|
content,
|
|
47
44
|
context: context,
|
|
48
45
|
});
|
|
49
|
-
const emoji =
|
|
46
|
+
const emoji = severityEmoji[result.severity] || '\u26AA';
|
|
50
47
|
const text = `## ${result.is_bullying ? '\u26A0\uFE0F Bullying Detected' : '\u2705 No Bullying Detected'}
|
|
51
48
|
|
|
52
49
|
**Severity:** ${emoji} ${result.severity.charAt(0).toUpperCase() + result.severity.slice(1)}
|
|
@@ -66,16 +63,16 @@ ${result.rationale}
|
|
|
66
63
|
};
|
|
67
64
|
});
|
|
68
65
|
// ── detect_grooming ────────────────────────────────────────────────────────
|
|
69
|
-
|
|
66
|
+
registerAppTool(server, 'detect_grooming', {
|
|
70
67
|
title: 'Detect Grooming',
|
|
71
68
|
description: 'Analyze a conversation for grooming patterns and predatory behavior.',
|
|
72
69
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
73
70
|
inputSchema: {
|
|
74
|
-
messages:
|
|
75
|
-
role:
|
|
76
|
-
content:
|
|
71
|
+
messages: z.array(z.object({
|
|
72
|
+
role: z.enum(['adult', 'child', 'unknown']),
|
|
73
|
+
content: z.string(),
|
|
77
74
|
})).describe('Array of messages in the conversation'),
|
|
78
|
-
childAge:
|
|
75
|
+
childAge: z.number().optional().describe('Age of the child in the conversation'),
|
|
79
76
|
},
|
|
80
77
|
_meta: uiMeta('Shows grooming detection results with risk indicators', 'Analyzing conversation for grooming patterns...', 'Grooming analysis complete.'),
|
|
81
78
|
}, async ({ messages, childAge }) => {
|
|
@@ -83,7 +80,7 @@ ${result.rationale}
|
|
|
83
80
|
messages,
|
|
84
81
|
childAge,
|
|
85
82
|
});
|
|
86
|
-
const emoji =
|
|
83
|
+
const emoji = riskEmoji[result.grooming_risk] || '\u26AA';
|
|
87
84
|
const text = `## ${result.grooming_risk === 'none' ? '\u2705 No Grooming Detected' : '\u26A0\uFE0F Grooming Risk Detected'}
|
|
88
85
|
|
|
89
86
|
**Risk Level:** ${emoji} ${result.grooming_risk.charAt(0).toUpperCase() + result.grooming_risk.slice(1)}
|
|
@@ -103,12 +100,12 @@ ${result.rationale}
|
|
|
103
100
|
};
|
|
104
101
|
});
|
|
105
102
|
// ── detect_unsafe ──────────────────────────────────────────────────────────
|
|
106
|
-
|
|
103
|
+
registerAppTool(server, 'detect_unsafe', {
|
|
107
104
|
title: 'Detect Unsafe Content',
|
|
108
105
|
description: 'Detect unsafe content including self-harm, violence, drugs, explicit material.',
|
|
109
106
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
110
107
|
inputSchema: {
|
|
111
|
-
content:
|
|
108
|
+
content: z.string().describe('The text content to analyze for unsafe content'),
|
|
112
109
|
context: contextSchema,
|
|
113
110
|
},
|
|
114
111
|
_meta: uiMeta('Shows unsafe content detection results', 'Analyzing content for safety concerns...', 'Safety analysis complete.'),
|
|
@@ -117,7 +114,7 @@ ${result.rationale}
|
|
|
117
114
|
content,
|
|
118
115
|
context: context,
|
|
119
116
|
});
|
|
120
|
-
const emoji =
|
|
117
|
+
const emoji = severityEmoji[result.severity] || '\u26AA';
|
|
121
118
|
const text = `## ${result.unsafe ? '\u26A0\uFE0F Unsafe Content Detected' : '\u2705 Content is Safe'}
|
|
122
119
|
|
|
123
120
|
**Severity:** ${emoji} ${result.severity.charAt(0).toUpperCase() + result.severity.slice(1)}
|
|
@@ -137,18 +134,18 @@ ${result.rationale}
|
|
|
137
134
|
};
|
|
138
135
|
});
|
|
139
136
|
// ── analyze (quick combined) ───────────────────────────────────────────────
|
|
140
|
-
|
|
137
|
+
registerAppTool(server, 'analyze', {
|
|
141
138
|
title: 'Quick Safety Analysis',
|
|
142
139
|
description: 'Quick comprehensive safety analysis that checks for both bullying and unsafe content.',
|
|
143
140
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
144
141
|
inputSchema: {
|
|
145
|
-
content:
|
|
146
|
-
include:
|
|
142
|
+
content: z.string().describe('The text content to analyze'),
|
|
143
|
+
include: z.array(z.enum(['bullying', 'unsafe'])).optional().describe('Which checks to run (default: both)'),
|
|
147
144
|
},
|
|
148
145
|
_meta: uiMeta('Shows combined safety analysis results', 'Running safety analysis...', 'Safety analysis complete.'),
|
|
149
146
|
}, async ({ content, include }) => {
|
|
150
147
|
const result = await client.analyze({ content, include });
|
|
151
|
-
const emoji =
|
|
148
|
+
const emoji = riskEmoji[result.risk_level] || '\u26AA';
|
|
152
149
|
const text = `## Safety Analysis Results
|
|
153
150
|
|
|
154
151
|
**Overall Risk:** ${emoji} ${result.risk_level.charAt(0).toUpperCase() + result.risk_level.slice(1)}
|
package/dist/src/tools/fraud.js
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
const server_1 = require("@modelcontextprotocol/ext-apps/server");
|
|
6
|
-
const formatters_js_1 = require("../formatters.js");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
|
3
|
+
import { formatDetectionResult } from '../formatters.js';
|
|
7
4
|
const DETECTION_WIDGET_URI = 'ui://tuteliq/detection-result.html';
|
|
8
5
|
const FRAUD_TOOLS = [
|
|
9
6
|
{
|
|
@@ -72,15 +69,15 @@ const FRAUD_TOOLS = [
|
|
|
72
69
|
},
|
|
73
70
|
];
|
|
74
71
|
const fraudInputSchema = {
|
|
75
|
-
content:
|
|
76
|
-
context:
|
|
77
|
-
include_evidence:
|
|
78
|
-
external_id:
|
|
79
|
-
customer_id:
|
|
72
|
+
content: z.string().describe('Text content to analyze'),
|
|
73
|
+
context: z.record(z.string(), z.unknown()).optional().describe('Optional analysis context'),
|
|
74
|
+
include_evidence: z.boolean().optional().describe('Include supporting evidence excerpts'),
|
|
75
|
+
external_id: z.string().optional().describe('External tracking ID'),
|
|
76
|
+
customer_id: z.string().optional().describe('Customer identifier'),
|
|
80
77
|
};
|
|
81
|
-
function registerFraudTools(server, client) {
|
|
78
|
+
export function registerFraudTools(server, client) {
|
|
82
79
|
for (const tool of FRAUD_TOOLS) {
|
|
83
|
-
|
|
80
|
+
registerAppTool(server, tool.name, {
|
|
84
81
|
title: tool.title,
|
|
85
82
|
description: tool.description,
|
|
86
83
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
@@ -102,7 +99,7 @@ function registerFraudTools(server, client) {
|
|
|
102
99
|
});
|
|
103
100
|
return {
|
|
104
101
|
structuredContent: { toolName: tool.name, result, branding: { appName: 'Tuteliq' } },
|
|
105
|
-
content: [{ type: 'text', text:
|
|
102
|
+
content: [{ type: 'text', text: formatDetectionResult(result) }],
|
|
106
103
|
};
|
|
107
104
|
});
|
|
108
105
|
}
|
package/dist/src/tools/media.js
CHANGED
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const fs_1 = require("fs");
|
|
7
|
-
const path_1 = require("path");
|
|
8
|
-
const formatters_js_1 = require("../formatters.js");
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
import { severityEmoji, trendEmoji, formatVideoResult } from '../formatters.js';
|
|
9
6
|
const MEDIA_WIDGET_URI = 'ui://tuteliq/media-result.html';
|
|
10
7
|
function loadWidget(name) {
|
|
11
|
-
return
|
|
8
|
+
return readFileSync(resolve(__dirname, '../../dist-ui', name), 'utf-8');
|
|
12
9
|
}
|
|
13
10
|
function filenameFromPath(filePath) {
|
|
14
11
|
return filePath.split('/').pop() || filePath;
|
|
15
12
|
}
|
|
16
|
-
function registerMediaTools(server, client) {
|
|
17
|
-
|
|
13
|
+
export function registerMediaTools(server, client) {
|
|
14
|
+
registerAppResource(server, MEDIA_WIDGET_URI, MEDIA_WIDGET_URI, { mimeType: RESOURCE_MIME_TYPE }, async () => ({
|
|
18
15
|
contents: [{
|
|
19
16
|
uri: MEDIA_WIDGET_URI,
|
|
20
|
-
mimeType:
|
|
17
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
21
18
|
text: loadWidget('media-result.html'),
|
|
22
19
|
}],
|
|
23
20
|
}));
|
|
24
21
|
// ── analyze_voice ──────────────────────────────────────────────────────────
|
|
25
|
-
|
|
22
|
+
registerAppTool(server, 'analyze_voice', {
|
|
26
23
|
title: 'Analyze Voice',
|
|
27
24
|
description: 'Analyze an audio file for safety concerns. Transcribes the audio via Whisper, then runs safety analysis on the transcript. Supports mp3, wav, m4a, ogg, flac, webm, mp4.',
|
|
28
25
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
29
26
|
inputSchema: {
|
|
30
|
-
file_path:
|
|
31
|
-
analysis_type:
|
|
32
|
-
child_age:
|
|
33
|
-
language:
|
|
27
|
+
file_path: z.string().describe('Absolute path to the audio file on disk'),
|
|
28
|
+
analysis_type: z.enum(['bullying', 'unsafe', 'grooming', 'emotions', 'all']).optional().describe('Type of analysis to run on the transcript (default: all)'),
|
|
29
|
+
child_age: z.number().optional().describe('Child age (used for grooming analysis)'),
|
|
30
|
+
language: z.string().optional().describe('Language hint for transcription (e.g., "en", "es")'),
|
|
34
31
|
},
|
|
35
32
|
_meta: {
|
|
36
33
|
ui: { resourceUri: MEDIA_WIDGET_URI },
|
|
@@ -39,7 +36,7 @@ function registerMediaTools(server, client) {
|
|
|
39
36
|
'openai/toolInvocation/invoked': 'Voice analysis complete.',
|
|
40
37
|
},
|
|
41
38
|
}, async ({ file_path, analysis_type, child_age, language }) => {
|
|
42
|
-
const buffer =
|
|
39
|
+
const buffer = readFileSync(file_path);
|
|
43
40
|
const filename = filenameFromPath(file_path);
|
|
44
41
|
const result = await client.analyzeVoice({
|
|
45
42
|
file: buffer,
|
|
@@ -48,7 +45,7 @@ function registerMediaTools(server, client) {
|
|
|
48
45
|
language,
|
|
49
46
|
childAge: child_age,
|
|
50
47
|
});
|
|
51
|
-
const emoji =
|
|
48
|
+
const emoji = severityEmoji[result.overall_severity] || '\u2705';
|
|
52
49
|
const segmentLines = result.transcription.segments
|
|
53
50
|
.slice(0, 20)
|
|
54
51
|
.map(s => `\`${s.start.toFixed(1)}s\u2013${s.end.toFixed(1)}s\` ${s.text}`)
|
|
@@ -64,7 +61,7 @@ function registerMediaTools(server, client) {
|
|
|
64
61
|
analysisLines.push(`**Grooming:** ${result.analysis.grooming.grooming_risk !== 'none' ? '\u26A0\uFE0F ' + result.analysis.grooming.grooming_risk : '\u2705 Clear'} (${(result.analysis.grooming.risk_score * 100).toFixed(0)}%)`);
|
|
65
62
|
}
|
|
66
63
|
if (result.analysis.emotions) {
|
|
67
|
-
analysisLines.push(`**Emotions:** ${result.analysis.emotions.dominant_emotions.join(', ')} (${
|
|
64
|
+
analysisLines.push(`**Emotions:** ${result.analysis.emotions.dominant_emotions.join(', ')} (${trendEmoji[result.analysis.emotions.trend] || ''} ${result.analysis.emotions.trend})`);
|
|
68
65
|
}
|
|
69
66
|
const text = `## \u{1F399}\uFE0F Voice Analysis
|
|
70
67
|
|
|
@@ -87,13 +84,13 @@ ${analysisLines.join('\n')}`;
|
|
|
87
84
|
};
|
|
88
85
|
});
|
|
89
86
|
// ── analyze_image ──────────────────────────────────────────────────────────
|
|
90
|
-
|
|
87
|
+
registerAppTool(server, 'analyze_image', {
|
|
91
88
|
title: 'Analyze Image',
|
|
92
89
|
description: 'Analyze an image for visual safety concerns and OCR text extraction. Supports png, jpg, jpeg, gif, webp.',
|
|
93
90
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
94
91
|
inputSchema: {
|
|
95
|
-
file_path:
|
|
96
|
-
analysis_type:
|
|
92
|
+
file_path: z.string().describe('Absolute path to the image file on disk'),
|
|
93
|
+
analysis_type: z.enum(['bullying', 'unsafe', 'emotions', 'all']).optional().describe('Type of analysis to run on extracted text (default: all)'),
|
|
97
94
|
},
|
|
98
95
|
_meta: {
|
|
99
96
|
ui: { resourceUri: MEDIA_WIDGET_URI },
|
|
@@ -102,14 +99,14 @@ ${analysisLines.join('\n')}`;
|
|
|
102
99
|
'openai/toolInvocation/invoked': 'Image analysis complete.',
|
|
103
100
|
},
|
|
104
101
|
}, async ({ file_path, analysis_type }) => {
|
|
105
|
-
const buffer =
|
|
102
|
+
const buffer = readFileSync(file_path);
|
|
106
103
|
const filename = filenameFromPath(file_path);
|
|
107
104
|
const result = await client.analyzeImage({
|
|
108
105
|
file: buffer,
|
|
109
106
|
filename,
|
|
110
107
|
analysisType: analysis_type || 'all',
|
|
111
108
|
});
|
|
112
|
-
const emoji =
|
|
109
|
+
const emoji = severityEmoji[result.overall_severity] || '\u2705';
|
|
113
110
|
const textAnalysisLines = [];
|
|
114
111
|
if (result.text_analysis?.bullying) {
|
|
115
112
|
textAnalysisLines.push(`**Bullying:** ${result.text_analysis.bullying.is_bullying ? '\u26A0\uFE0F Detected' : '\u2705 Clear'} (${(result.text_analysis.bullying.risk_score * 100).toFixed(0)}%)`);
|
|
@@ -127,7 +124,7 @@ ${analysisLines.join('\n')}`;
|
|
|
127
124
|
|
|
128
125
|
### Vision Results
|
|
129
126
|
**Description:** ${result.vision.visual_description}
|
|
130
|
-
**Visual Severity:** ${
|
|
127
|
+
**Visual Severity:** ${severityEmoji[result.vision.visual_severity] || '\u2705'} ${result.vision.visual_severity}
|
|
131
128
|
**Visual Confidence:** ${(result.vision.visual_confidence * 100).toFixed(0)}%
|
|
132
129
|
**Contains Text:** ${result.vision.contains_text ? 'Yes' : 'No'}
|
|
133
130
|
**Contains Faces:** ${result.vision.contains_faces ? 'Yes' : 'No'}
|
|
@@ -142,13 +139,13 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
|
|
|
142
139
|
};
|
|
143
140
|
});
|
|
144
141
|
// ── analyze_video ──────────────────────────────────────────────────────────
|
|
145
|
-
|
|
142
|
+
registerAppTool(server, 'analyze_video', {
|
|
146
143
|
title: 'Analyze Video',
|
|
147
144
|
description: 'Analyze a video file for safety concerns. Extracts key frames and runs safety classification. Supports mp4, mov, avi, webm, mkv.',
|
|
148
145
|
annotations: { readOnlyHint: true, openWorldHint: true, destructiveHint: false },
|
|
149
146
|
inputSchema: {
|
|
150
|
-
file_path:
|
|
151
|
-
age_group:
|
|
147
|
+
file_path: z.string().describe('Absolute path to the video file on disk'),
|
|
148
|
+
age_group: z.string().optional().describe('Age group for calibrated analysis (e.g., "child", "teen", "adult")'),
|
|
152
149
|
},
|
|
153
150
|
_meta: {
|
|
154
151
|
ui: { resourceUri: MEDIA_WIDGET_URI },
|
|
@@ -157,7 +154,7 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
|
|
|
157
154
|
'openai/toolInvocation/invoked': 'Video analysis complete.',
|
|
158
155
|
},
|
|
159
156
|
}, async ({ file_path, age_group }) => {
|
|
160
|
-
const buffer =
|
|
157
|
+
const buffer = readFileSync(file_path);
|
|
161
158
|
const filename = filenameFromPath(file_path);
|
|
162
159
|
const result = await client.analyzeVideo({
|
|
163
160
|
file: buffer,
|
|
@@ -166,7 +163,7 @@ ${textAnalysisLines.length > 0 ? `### Text Analysis Results\n${textAnalysisLines
|
|
|
166
163
|
});
|
|
167
164
|
return {
|
|
168
165
|
structuredContent: { toolName: 'analyze_video', result, branding: { appName: 'Tuteliq' } },
|
|
169
|
-
content: [{ type: 'text', text:
|
|
166
|
+
content: [{ type: 'text', text: formatVideoResult(result) }],
|
|
170
167
|
};
|
|
171
168
|
});
|
|
172
169
|
}
|
package/dist/src/transport.d.ts
CHANGED
|
@@ -3,5 +3,10 @@ import type { Request, Response } from 'express';
|
|
|
3
3
|
export type TransportMode = 'http' | 'stdio';
|
|
4
4
|
export declare function getTransportMode(): TransportMode;
|
|
5
5
|
export declare function startStdio(server: McpServer): Promise<void>;
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function createHttpHandlers(createServer: () => McpServer): {
|
|
7
|
+
streamableHandler: (req: Request, res: Response) => Promise<void>;
|
|
8
|
+
sseHandler: (req: Request, res: Response) => Promise<void>;
|
|
9
|
+
messagesHandler: (req: Request, res: Response) => Promise<void>;
|
|
10
|
+
closeAll: () => Promise<void>;
|
|
11
|
+
};
|
|
7
12
|
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAU7C,wBAAgB,gBAAgB,IAAI,aAAa,CAIhD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAIjE;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,SAAS;6BAmCxB,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;sBA6E7C,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;2BAajC,OAAO,OAAO,QAAQ,KAAG,OAAO,CAAC,IAAI,CAAC;oBAa/C,OAAO,CAAC,IAAI,CAAC;EAazC"}
|
package/dist/src/transport.js
CHANGED
|
@@ -1,53 +1,119 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
function getTransportMode() {
|
|
1
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
4
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
const SESSION_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // check every 5 minutes
|
|
7
|
+
export function getTransportMode() {
|
|
9
8
|
const env = process.env.TUTELIQ_MCP_TRANSPORT?.toLowerCase();
|
|
10
9
|
if (env === 'stdio')
|
|
11
10
|
return 'stdio';
|
|
12
11
|
return 'http';
|
|
13
12
|
}
|
|
14
|
-
async function startStdio(server) {
|
|
15
|
-
const transport = new
|
|
13
|
+
export async function startStdio(server) {
|
|
14
|
+
const transport = new StdioServerTransport();
|
|
16
15
|
await server.connect(transport);
|
|
17
16
|
console.error('Tuteliq MCP server running on stdio');
|
|
18
17
|
}
|
|
19
|
-
function
|
|
20
|
-
const
|
|
21
|
-
|
|
18
|
+
export function createHttpHandlers(createServer) {
|
|
19
|
+
const sessions = new Map();
|
|
20
|
+
function addSession(id, transport) {
|
|
21
|
+
sessions.set(id, { transport, lastActivity: Date.now() });
|
|
22
|
+
}
|
|
23
|
+
function removeSession(id) {
|
|
24
|
+
sessions.delete(id);
|
|
25
|
+
}
|
|
26
|
+
function getTransport(id) {
|
|
27
|
+
const entry = sessions.get(id);
|
|
28
|
+
if (entry) {
|
|
29
|
+
entry.lastActivity = Date.now();
|
|
30
|
+
return entry.transport;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
// Periodic cleanup of stale sessions
|
|
35
|
+
const cleanupTimer = setInterval(async () => {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
for (const [id, entry] of sessions) {
|
|
38
|
+
if (now - entry.lastActivity > SESSION_TTL_MS) {
|
|
39
|
+
try {
|
|
40
|
+
await entry.transport.close();
|
|
41
|
+
}
|
|
42
|
+
catch { /* already closed */ }
|
|
43
|
+
sessions.delete(id);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}, CLEANUP_INTERVAL_MS);
|
|
47
|
+
cleanupTimer.unref();
|
|
48
|
+
// Streamable HTTP handler — POST /mcp, GET /mcp, DELETE /mcp
|
|
49
|
+
const streamableHandler = async (req, res) => {
|
|
22
50
|
const sessionId = req.headers['mcp-session-id'];
|
|
23
51
|
if (req.method === 'POST') {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
if (sessionId) {
|
|
53
|
+
const existing = getTransport(sessionId);
|
|
54
|
+
if (existing) {
|
|
55
|
+
if (existing instanceof StreamableHTTPServerTransport) {
|
|
56
|
+
await existing.handleRequest(req, res, req.body);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.status(400).json({
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
error: { code: -32000, message: 'Session uses a different transport protocol' },
|
|
62
|
+
id: null,
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (!sessionId && isInitializeRequest(req.body)) {
|
|
68
|
+
const transport = new StreamableHTTPServerTransport({
|
|
69
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
70
|
+
onsessioninitialized: (id) => {
|
|
71
|
+
addSession(id, transport);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
transport.onclose = () => {
|
|
75
|
+
const sid = transport.sessionId;
|
|
76
|
+
if (sid)
|
|
77
|
+
removeSession(sid);
|
|
78
|
+
};
|
|
79
|
+
const server = createServer();
|
|
80
|
+
await server.connect(transport);
|
|
81
|
+
await transport.handleRequest(req, res, req.body);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
res.status(400).json({
|
|
85
|
+
jsonrpc: '2.0',
|
|
86
|
+
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
|
|
87
|
+
id: null,
|
|
29
88
|
});
|
|
30
|
-
transport.onclose = () => {
|
|
31
|
-
const id = [...transports.entries()].find(([, t]) => t === transport)?.[0];
|
|
32
|
-
if (id)
|
|
33
|
-
transports.delete(id);
|
|
34
|
-
};
|
|
35
|
-
await server.connect(transport);
|
|
36
|
-
await transport.handleRequest(req, res, req.body);
|
|
37
89
|
}
|
|
38
90
|
else if (req.method === 'GET') {
|
|
39
|
-
if (!sessionId
|
|
91
|
+
if (!sessionId) {
|
|
40
92
|
res.status(400).json({ error: 'Invalid or missing session ID' });
|
|
41
93
|
return;
|
|
42
94
|
}
|
|
43
|
-
const transport =
|
|
44
|
-
|
|
95
|
+
const transport = getTransport(sessionId);
|
|
96
|
+
if (!transport) {
|
|
97
|
+
res.status(400).json({ error: 'Invalid or missing session ID' });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (transport instanceof StreamableHTTPServerTransport) {
|
|
101
|
+
await transport.handleRequest(req, res);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
res.status(400).json({ error: 'Session uses a different transport protocol' });
|
|
105
|
+
}
|
|
45
106
|
}
|
|
46
107
|
else if (req.method === 'DELETE') {
|
|
47
|
-
if (sessionId
|
|
48
|
-
const transport =
|
|
49
|
-
|
|
50
|
-
|
|
108
|
+
if (sessionId) {
|
|
109
|
+
const transport = getTransport(sessionId);
|
|
110
|
+
if (transport && transport instanceof StreamableHTTPServerTransport) {
|
|
111
|
+
await transport.handleRequest(req, res);
|
|
112
|
+
removeSession(sessionId);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
res.status(400).json({ error: 'Invalid or missing session ID' });
|
|
116
|
+
}
|
|
51
117
|
}
|
|
52
118
|
else {
|
|
53
119
|
res.status(400).json({ error: 'Invalid or missing session ID' });
|
|
@@ -57,4 +123,38 @@ function createHttpHandler(server) {
|
|
|
57
123
|
res.status(405).json({ error: 'Method not allowed' });
|
|
58
124
|
}
|
|
59
125
|
};
|
|
126
|
+
// Legacy SSE handler — GET /sse
|
|
127
|
+
const sseHandler = async (req, res) => {
|
|
128
|
+
const transport = new SSEServerTransport('/messages', res);
|
|
129
|
+
addSession(transport.sessionId, transport);
|
|
130
|
+
res.on('close', () => {
|
|
131
|
+
removeSession(transport.sessionId);
|
|
132
|
+
});
|
|
133
|
+
const server = createServer();
|
|
134
|
+
await server.connect(transport);
|
|
135
|
+
};
|
|
136
|
+
// Legacy message handler — POST /messages?sessionId=...
|
|
137
|
+
const messagesHandler = async (req, res) => {
|
|
138
|
+
const sessionId = req.query.sessionId;
|
|
139
|
+
const transport = getTransport(sessionId);
|
|
140
|
+
if (!transport || !(transport instanceof SSEServerTransport)) {
|
|
141
|
+
res.status(400).json({ error: 'Invalid or missing session ID' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await transport.handlePostMessage(req, res, req.body);
|
|
145
|
+
};
|
|
146
|
+
// Graceful shutdown
|
|
147
|
+
const closeAll = async () => {
|
|
148
|
+
clearInterval(cleanupTimer);
|
|
149
|
+
for (const [id, entry] of sessions) {
|
|
150
|
+
try {
|
|
151
|
+
await entry.transport.close();
|
|
152
|
+
sessions.delete(id);
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(`Error closing transport ${id}:`, error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
return { streamableHandler, sseHandler, messagesHandler, closeAll };
|
|
60
160
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuteliq/mcp",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "MCP App server for Tuteliq - AI-powered child safety analysis with interactive UI widgets for Claude, ChatGPT, and other MCP hosts",
|
|
5
|
+
"type": "module",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
7
8
|
"bin": {
|