@vespermcp/mcp-server 1.3.0 → 1.3.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/build/index.js +1327 -1318
- package/build/lib/mcp-analytics.js +164 -0
- package/build/lib/plan-resolve.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { getSupabaseAdminClient, resolveUserIdFromApiKey } from "./plan-resolve.js";
|
|
2
|
+
import { inferSourceFromDatasetId, normalizeProviderSource, PLAN_GATE_EXEMPT_TOOLS, } from "./plan-gate.js";
|
|
3
|
+
function truncateDatasetName(raw) {
|
|
4
|
+
const t = raw.trim();
|
|
5
|
+
if (!t)
|
|
6
|
+
return "unknown";
|
|
7
|
+
return t.length > 256 ? t.slice(0, 253) + "..." : t;
|
|
8
|
+
}
|
|
9
|
+
function pickDatasetName(args) {
|
|
10
|
+
const a = args || {};
|
|
11
|
+
const candidates = [
|
|
12
|
+
a.dataset_id,
|
|
13
|
+
a.query,
|
|
14
|
+
a.datasetId,
|
|
15
|
+
a.url,
|
|
16
|
+
a.file_path,
|
|
17
|
+
];
|
|
18
|
+
for (const c of candidates) {
|
|
19
|
+
if (typeof c === "string" && c.trim()) {
|
|
20
|
+
return truncateDatasetName(c);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return "unknown";
|
|
24
|
+
}
|
|
25
|
+
function pickSource(toolName, args) {
|
|
26
|
+
const a = args || {};
|
|
27
|
+
const explicit = normalizeProviderSource(String(a.source ?? ""));
|
|
28
|
+
if (explicit)
|
|
29
|
+
return explicit;
|
|
30
|
+
const fromId = inferSourceFromDatasetId(String(a.dataset_id ?? a.query ?? ""));
|
|
31
|
+
if (fromId)
|
|
32
|
+
return fromId;
|
|
33
|
+
if (toolName === "vesper_web_find" && Array.isArray(a.sources) && a.sources.length > 0) {
|
|
34
|
+
return String(a.sources[0]).toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function pickFormat(args) {
|
|
39
|
+
const a = args || {};
|
|
40
|
+
const f = a.format ?? a.target_format ?? a.output_format;
|
|
41
|
+
if (typeof f === "string" && f.trim())
|
|
42
|
+
return f.trim().toLowerCase().slice(0, 32);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function mapToolToEvent(toolName) {
|
|
46
|
+
switch (toolName) {
|
|
47
|
+
case "vesper_search":
|
|
48
|
+
case "discover_datasets":
|
|
49
|
+
case "vesper_web_find":
|
|
50
|
+
case "vesper.extract_web":
|
|
51
|
+
case "get_dataset_info":
|
|
52
|
+
return { event_type: "dataset_search" };
|
|
53
|
+
case "download_dataset":
|
|
54
|
+
case "vesper_download_assets":
|
|
55
|
+
return { event_type: "dataset_download" };
|
|
56
|
+
case "quality_analyze":
|
|
57
|
+
case "analyze_quality":
|
|
58
|
+
case "analyze_image_quality":
|
|
59
|
+
case "analyze_media_quality":
|
|
60
|
+
case "generate_quality_report":
|
|
61
|
+
case "preview_cleaning":
|
|
62
|
+
return { event_type: "quality_analysis" };
|
|
63
|
+
case "prepare_dataset":
|
|
64
|
+
return { event_type: "dataset_prepare" };
|
|
65
|
+
case "export_dataset":
|
|
66
|
+
case "vesper_convert_format":
|
|
67
|
+
return { event_type: "export" };
|
|
68
|
+
default:
|
|
69
|
+
return { event_type: "data_processed" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function mapToolToEventWithArgs(toolName, args) {
|
|
73
|
+
if (toolName === "unified_dataset_api") {
|
|
74
|
+
const op = String(args.operation ?? "").trim().toLowerCase();
|
|
75
|
+
if (op === "discover" || op === "providers" || op === "info") {
|
|
76
|
+
return { event_type: "dataset_search" };
|
|
77
|
+
}
|
|
78
|
+
if (op === "download") {
|
|
79
|
+
return { event_type: "dataset_download" };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return mapToolToEvent(toolName);
|
|
83
|
+
}
|
|
84
|
+
async function hasAnalyticsConsent(userId) {
|
|
85
|
+
const supabase = getSupabaseAdminClient();
|
|
86
|
+
if (!supabase)
|
|
87
|
+
return false;
|
|
88
|
+
const { data, error } = await supabase
|
|
89
|
+
.from("analytics_consent")
|
|
90
|
+
.select("consented")
|
|
91
|
+
.eq("user_id", userId)
|
|
92
|
+
.maybeSingle();
|
|
93
|
+
if (error) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return data?.consented === true;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* After each MCP tool call: insert one row into `analytics_events` (same table as
|
|
100
|
+
* `/api/analytics/ingest` and the landing Operations tab) when the user has opted in
|
|
101
|
+
* and `VESPER_API_KEY` / `api_key` resolves to a user.
|
|
102
|
+
*/
|
|
103
|
+
export async function recordMcpToolAnalyticsAfterCall(opts) {
|
|
104
|
+
if (process.env.VESPER_DISABLE_MCP_ANALYTICS === "1" || process.env.VESPER_DISABLE_MCP_ANALYTICS === "true") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const toolName = String(opts.toolName || "").trim();
|
|
108
|
+
if (!toolName || PLAN_GATE_EXEMPT_TOOLS.has(toolName)) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const supabase = getSupabaseAdminClient();
|
|
112
|
+
if (!supabase) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const args = opts.args || {};
|
|
116
|
+
const apiKey = String(args.api_key ?? process.env.VESPER_API_KEY ?? "").trim();
|
|
117
|
+
if (!apiKey) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const userId = await resolveUserIdFromApiKey(apiKey);
|
|
121
|
+
if (!userId) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!(await hasAnalyticsConsent(userId))) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const { event_type } = mapToolToEventWithArgs(toolName, args);
|
|
128
|
+
const dataset_name = pickDatasetName(args);
|
|
129
|
+
const source = pickSource(toolName, args);
|
|
130
|
+
const format = pickFormat(args);
|
|
131
|
+
const metadata = {
|
|
132
|
+
mcp_tool: toolName,
|
|
133
|
+
ok: opts.result !== undefined && opts.result.isError !== true,
|
|
134
|
+
};
|
|
135
|
+
if (opts.result?.isError) {
|
|
136
|
+
const first = opts.result.content?.[0];
|
|
137
|
+
if (first && typeof first.text === "string") {
|
|
138
|
+
metadata.error_preview = first.text.slice(0, 500);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const row = {
|
|
142
|
+
user_id: userId,
|
|
143
|
+
event_type,
|
|
144
|
+
dataset_name,
|
|
145
|
+
source: source || null,
|
|
146
|
+
format: format || null,
|
|
147
|
+
size_bytes: null,
|
|
148
|
+
quality_score: null,
|
|
149
|
+
metadata,
|
|
150
|
+
created_at: new Date().toISOString(),
|
|
151
|
+
};
|
|
152
|
+
let { error } = await supabase.from("analytics_events").insert(row);
|
|
153
|
+
// Older DBs may lack `dataset_prepare` in CHECK constraint — fall back.
|
|
154
|
+
if (error && event_type === "dataset_prepare" && /check|constraint/i.test(error.message || "")) {
|
|
155
|
+
({ error } = await supabase.from("analytics_events").insert({
|
|
156
|
+
...row,
|
|
157
|
+
event_type: "data_processed",
|
|
158
|
+
metadata: { ...metadata, event_type_fallback: "dataset_prepare" },
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
if (error) {
|
|
162
|
+
console.error("[mcp-analytics] insert failed:", error.message);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -45,7 +45,7 @@ async function getUserPlanForUserId(userId) {
|
|
|
45
45
|
}
|
|
46
46
|
return "free";
|
|
47
47
|
}
|
|
48
|
-
async function
|
|
48
|
+
async function lookupUserByApiKey(apiKey) {
|
|
49
49
|
const supabase = getSupabase();
|
|
50
50
|
if (!supabase)
|
|
51
51
|
return null;
|
|
@@ -59,6 +59,14 @@ async function resolveUserFromApiKey(apiKey) {
|
|
|
59
59
|
}
|
|
60
60
|
return { userId: data.user_id };
|
|
61
61
|
}
|
|
62
|
+
/** For analytics / profile sync — same lookup as plan gate. */
|
|
63
|
+
export async function resolveUserIdFromApiKey(apiKey) {
|
|
64
|
+
const row = await lookupUserByApiKey(apiKey);
|
|
65
|
+
return row?.userId ?? null;
|
|
66
|
+
}
|
|
67
|
+
export function getSupabaseAdminClient() {
|
|
68
|
+
return getSupabase();
|
|
69
|
+
}
|
|
62
70
|
/**
|
|
63
71
|
* Central gate: same rules as `landing/lib/plan-entitlements` + landing analytics ingest.
|
|
64
72
|
*/
|
|
@@ -77,7 +85,7 @@ export async function enforcePlanGateForTool(toolName, args) {
|
|
|
77
85
|
message: "Plan enforcement is enabled (Supabase configured). Set `VESPER_API_KEY` in the MCP env or pass `api_key` on tool calls to your Vesper API key so your tier can be verified.",
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
|
-
const user = await
|
|
88
|
+
const user = await lookupUserByApiKey(apiKey);
|
|
81
89
|
if (!user) {
|
|
82
90
|
return {
|
|
83
91
|
ok: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vespermcp/mcp-server",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "AI-powered dataset discovery, quality analysis, and preparation MCP server with multimodal support (text, image, audio, video)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|