@easyfunnel/mcp 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +150 -47
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -94,6 +94,14 @@ var ApiClient = class {
|
|
|
94
94
|
body: JSON.stringify(config)
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
+
async getConversations(projectId, params) {
|
|
98
|
+
const searchParams = new URLSearchParams();
|
|
99
|
+
if (params.limit) searchParams.set("limit", params.limit.toString());
|
|
100
|
+
if (params.session_id) searchParams.set("session_id", params.session_id);
|
|
101
|
+
return this.request(
|
|
102
|
+
`/projects/${projectId}/conversations?${searchParams.toString()}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
97
105
|
async queryEvents(projectId, params) {
|
|
98
106
|
const searchParams = new URLSearchParams();
|
|
99
107
|
searchParams.set("query_type", params.query_type);
|
|
@@ -211,25 +219,25 @@ var frameworkConfigs = {
|
|
|
211
219
|
nextjs: {
|
|
212
220
|
envFile: ".env.local",
|
|
213
221
|
envVarName: "NEXT_PUBLIC_EASYFUNNEL_KEY",
|
|
214
|
-
envAccessor:
|
|
222
|
+
envAccessor: (apiKey2) => `process.env.NEXT_PUBLIC_EASYFUNNEL_KEY || "${apiKey2}"`,
|
|
215
223
|
layoutPaths: ["app/layout.tsx", "app/layout.jsx", "src/app/layout.tsx", "src/app/layout.jsx"]
|
|
216
224
|
},
|
|
217
225
|
vite: {
|
|
218
226
|
envFile: ".env",
|
|
219
227
|
envVarName: "VITE_EASYFUNNEL_KEY",
|
|
220
|
-
envAccessor:
|
|
228
|
+
envAccessor: (apiKey2) => `import.meta.env.VITE_EASYFUNNEL_KEY || "${apiKey2}"`,
|
|
221
229
|
layoutPaths: ["src/App.tsx", "src/App.jsx", "src/main.tsx", "src/main.jsx"]
|
|
222
230
|
},
|
|
223
231
|
cra: {
|
|
224
232
|
envFile: ".env",
|
|
225
233
|
envVarName: "REACT_APP_EASYFUNNEL_KEY",
|
|
226
|
-
envAccessor:
|
|
234
|
+
envAccessor: (apiKey2) => `process.env.REACT_APP_EASYFUNNEL_KEY || "${apiKey2}"`,
|
|
227
235
|
layoutPaths: ["src/App.tsx", "src/App.jsx", "src/index.tsx", "src/index.jsx"]
|
|
228
236
|
},
|
|
229
237
|
sveltekit: {
|
|
230
238
|
envFile: ".env",
|
|
231
239
|
envVarName: "PUBLIC_EASYFUNNEL_KEY",
|
|
232
|
-
envAccessor:
|
|
240
|
+
envAccessor: (apiKey2) => `import.meta.env.PUBLIC_EASYFUNNEL_KEY || "${apiKey2}"`,
|
|
233
241
|
layoutPaths: ["src/routes/+layout.svelte"]
|
|
234
242
|
}
|
|
235
243
|
};
|
|
@@ -374,7 +382,7 @@ Next: After adding the script, I'll verify everything works with a test event.`
|
|
|
374
382
|
if (content.includes("{children}")) {
|
|
375
383
|
content = content.replace(
|
|
376
384
|
/(\{children\})/,
|
|
377
|
-
`<EasyFunnelProvider apiKey={${config.envAccessor}}>
|
|
385
|
+
`<EasyFunnelProvider apiKey={${config.envAccessor(project_api_key)}}>
|
|
378
386
|
$1
|
|
379
387
|
</EasyFunnelProvider>`
|
|
380
388
|
);
|
|
@@ -416,10 +424,12 @@ Project API Key: ${project_api_key}
|
|
|
416
424
|
output += `(This is the PROJECT key for the SDK \u2014 not the account key used by the MCP server.)
|
|
417
425
|
`;
|
|
418
426
|
output += `
|
|
419
|
-
|
|
427
|
+
The API key is hardcoded in your layout file \u2014 no env var configuration needed.
|
|
428
|
+
`;
|
|
429
|
+
output += `If you prefer, you can override it via ${config.envVarName} in ${config.envFile}.
|
|
420
430
|
`;
|
|
421
431
|
output += `
|
|
422
|
-
Next:
|
|
432
|
+
Next: I'll verify everything works with a test event.`;
|
|
423
433
|
return {
|
|
424
434
|
content: [{ type: "text", text: output }]
|
|
425
435
|
};
|
|
@@ -1636,18 +1646,18 @@ function readEnvFile(projectRoot) {
|
|
|
1636
1646
|
}
|
|
1637
1647
|
return null;
|
|
1638
1648
|
}
|
|
1649
|
+
var layoutCandidates = [
|
|
1650
|
+
"app/layout.tsx",
|
|
1651
|
+
"app/layout.jsx",
|
|
1652
|
+
"src/app/layout.tsx",
|
|
1653
|
+
"src/app/layout.jsx",
|
|
1654
|
+
"src/App.tsx",
|
|
1655
|
+
"src/App.jsx",
|
|
1656
|
+
"src/main.tsx",
|
|
1657
|
+
"src/main.jsx"
|
|
1658
|
+
];
|
|
1639
1659
|
function findProviderFile(projectRoot) {
|
|
1640
|
-
const
|
|
1641
|
-
"app/layout.tsx",
|
|
1642
|
-
"app/layout.jsx",
|
|
1643
|
-
"src/app/layout.tsx",
|
|
1644
|
-
"src/app/layout.jsx",
|
|
1645
|
-
"src/App.tsx",
|
|
1646
|
-
"src/App.jsx",
|
|
1647
|
-
"src/main.tsx",
|
|
1648
|
-
"src/main.jsx"
|
|
1649
|
-
];
|
|
1650
|
-
for (const relPath of candidates) {
|
|
1660
|
+
for (const relPath of layoutCandidates) {
|
|
1651
1661
|
const fullPath = (0, import_path4.join)(projectRoot, relPath);
|
|
1652
1662
|
if ((0, import_fs6.existsSync)(fullPath)) {
|
|
1653
1663
|
const content = (0, import_fs6.readFileSync)(fullPath, "utf-8");
|
|
@@ -1658,37 +1668,50 @@ function findProviderFile(projectRoot) {
|
|
|
1658
1668
|
}
|
|
1659
1669
|
return null;
|
|
1660
1670
|
}
|
|
1671
|
+
function findHardcodedKey(projectRoot) {
|
|
1672
|
+
for (const relPath of layoutCandidates) {
|
|
1673
|
+
const fullPath = (0, import_path4.join)(projectRoot, relPath);
|
|
1674
|
+
if (!(0, import_fs6.existsSync)(fullPath)) continue;
|
|
1675
|
+
const content = (0, import_fs6.readFileSync)(fullPath, "utf-8");
|
|
1676
|
+
if (!content.includes("EasyFunnelProvider")) continue;
|
|
1677
|
+
const match = content.match(/apiKey[=:].*?(ef_[a-f0-9]+)/);
|
|
1678
|
+
if (match) {
|
|
1679
|
+
return { file: relPath, key: match[1] };
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1661
1684
|
async function validateSetup(client2, args) {
|
|
1662
1685
|
const { project_root, project_id } = args;
|
|
1663
1686
|
const checks = [];
|
|
1664
1687
|
const envResult = readEnvFile(project_root);
|
|
1688
|
+
const hardcodedResult = findHardcodedKey(project_root);
|
|
1665
1689
|
let apiKey2 = args.project_api_key || "";
|
|
1666
|
-
if (envResult) {
|
|
1690
|
+
if (envResult && envResult.value && envResult.value.startsWith("ef_")) {
|
|
1667
1691
|
apiKey2 = apiKey2 || envResult.value;
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
}
|
|
1692
|
+
checks.push({
|
|
1693
|
+
name: `${envResult.file} contains ${envResult.varName}`,
|
|
1694
|
+
passed: true,
|
|
1695
|
+
detail: `${envResult.varName}=${envResult.value.slice(0, 8)}...`
|
|
1696
|
+
});
|
|
1697
|
+
} else if (hardcodedResult) {
|
|
1698
|
+
apiKey2 = apiKey2 || hardcodedResult.key;
|
|
1699
|
+
checks.push({
|
|
1700
|
+
name: `API key found in ${hardcodedResult.file}`,
|
|
1701
|
+
passed: true,
|
|
1702
|
+
detail: `Hardcoded key: ${hardcodedResult.key.slice(0, 8)}... (recommended for static deployments)`
|
|
1703
|
+
});
|
|
1704
|
+
} else if (envResult && envResult.value) {
|
|
1705
|
+
checks.push({
|
|
1706
|
+
name: `${envResult.file} contains ${envResult.varName}`,
|
|
1707
|
+
passed: false,
|
|
1708
|
+
detail: `Value doesn't start with "ef_". Got: ${envResult.value.slice(0, 20)}`
|
|
1709
|
+
});
|
|
1687
1710
|
} else {
|
|
1688
1711
|
checks.push({
|
|
1689
|
-
name: "
|
|
1712
|
+
name: "API key configured",
|
|
1690
1713
|
passed: false,
|
|
1691
|
-
detail: "No EasyFunnel API key found in
|
|
1714
|
+
detail: "No EasyFunnel API key found in env file or hardcoded in layout. Run setup_sdk to add it."
|
|
1692
1715
|
});
|
|
1693
1716
|
}
|
|
1694
1717
|
const providerFile = findProviderFile(project_root);
|
|
@@ -1789,14 +1812,14 @@ Next: Let me suggest conversion funnels based on what I found in your codebase.`
|
|
|
1789
1812
|
output += `
|
|
1790
1813
|
`;
|
|
1791
1814
|
for (const check of failedChecks) {
|
|
1792
|
-
if (check.name.includes("
|
|
1793
|
-
|
|
1794
|
-
const envFile = envResult?.file || ".env.local";
|
|
1795
|
-
output += `To fix: Add your project API key to ${envFile}:
|
|
1815
|
+
if (check.name.includes("API key") || check.name.includes("Env file")) {
|
|
1816
|
+
output += `To fix: Run setup_sdk to hardcode the API key in your layout file.
|
|
1796
1817
|
`;
|
|
1797
|
-
output += `
|
|
1818
|
+
output += `Alternatively, add it to an env file:
|
|
1798
1819
|
`;
|
|
1799
|
-
|
|
1820
|
+
const varName = envResult?.varName || "NEXT_PUBLIC_EASYFUNNEL_KEY";
|
|
1821
|
+
const envFile = envResult?.file || ".env.local";
|
|
1822
|
+
output += ` ${varName}=ef_your_key_here (in ${envFile})
|
|
1800
1823
|
|
|
1801
1824
|
`;
|
|
1802
1825
|
} else if (check.name.includes("Provider")) {
|
|
@@ -2102,6 +2125,83 @@ The chat widget will appear as a floating bubble in the bottom-right corner of y
|
|
|
2102
2125
|
};
|
|
2103
2126
|
}
|
|
2104
2127
|
|
|
2128
|
+
// src/tools/query-conversations.ts
|
|
2129
|
+
var queryConversationsDefinition = {
|
|
2130
|
+
name: "query_conversations",
|
|
2131
|
+
description: "Read chat widget conversations from visitors. Lists recent conversations with previews, or fetches full message transcript for a specific session.",
|
|
2132
|
+
inputSchema: {
|
|
2133
|
+
type: "object",
|
|
2134
|
+
properties: {
|
|
2135
|
+
project_id: { type: "string", description: "Project ID" },
|
|
2136
|
+
session_id: {
|
|
2137
|
+
type: "string",
|
|
2138
|
+
description: "Specific chat session ID to fetch full transcript. If omitted, lists recent conversations."
|
|
2139
|
+
},
|
|
2140
|
+
limit: {
|
|
2141
|
+
type: "number",
|
|
2142
|
+
description: "Max conversations to return (default: 20, max: 50)"
|
|
2143
|
+
}
|
|
2144
|
+
},
|
|
2145
|
+
required: ["project_id"]
|
|
2146
|
+
}
|
|
2147
|
+
};
|
|
2148
|
+
async function queryConversations(client2, args) {
|
|
2149
|
+
const data = await client2.getConversations(args.project_id, {
|
|
2150
|
+
limit: args.limit,
|
|
2151
|
+
session_id: args.session_id
|
|
2152
|
+
});
|
|
2153
|
+
let output;
|
|
2154
|
+
if (args.session_id) {
|
|
2155
|
+
const s = data.session;
|
|
2156
|
+
output = `Conversation with ${s.user_email || "Anonymous (" + s.visitor_id.slice(0, 8) + ")"}
|
|
2157
|
+
`;
|
|
2158
|
+
output += `Country: ${s.country || "unknown"} | Device: ${s.device || "unknown"} | Browser: ${s.browser || "unknown"}
|
|
2159
|
+
`;
|
|
2160
|
+
output += `First page: ${s.first_page || "unknown"}
|
|
2161
|
+
`;
|
|
2162
|
+
output += `Started: ${s.created_at}
|
|
2163
|
+
`;
|
|
2164
|
+
output += `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2165
|
+
|
|
2166
|
+
`;
|
|
2167
|
+
for (const msg of data.messages || []) {
|
|
2168
|
+
const label = msg.role === "user" ? "\u{1F464} Visitor" : "\u{1F916} Assistant";
|
|
2169
|
+
output += `${label}:
|
|
2170
|
+
${msg.content}
|
|
2171
|
+
`;
|
|
2172
|
+
if (msg.page_url) output += ` [on ${msg.page_url}]
|
|
2173
|
+
`;
|
|
2174
|
+
output += "\n";
|
|
2175
|
+
}
|
|
2176
|
+
if (!data.messages?.length) output += "No messages in this conversation.\n";
|
|
2177
|
+
} else {
|
|
2178
|
+
const sessions = data.sessions || [];
|
|
2179
|
+
output = `Chat conversations (${sessions.length} total):
|
|
2180
|
+
|
|
2181
|
+
`;
|
|
2182
|
+
for (const s of sessions) {
|
|
2183
|
+
const visitor = s.user_email || `Anonymous (${s.visitor_id.slice(0, 8)})`;
|
|
2184
|
+
const country = s.country || "??";
|
|
2185
|
+
const device = s.device || "?";
|
|
2186
|
+
output += `\u2022 ${visitor} \u2014 ${country}, ${device}, ${s.message_count} msgs
|
|
2187
|
+
`;
|
|
2188
|
+
output += ` ID: ${s.id}
|
|
2189
|
+
`;
|
|
2190
|
+
if (s.preview) output += ` "${s.preview}"
|
|
2191
|
+
`;
|
|
2192
|
+
output += ` First page: ${s.first_page || "?"} | Last active: ${s.last_message_at}
|
|
2193
|
+
|
|
2194
|
+
`;
|
|
2195
|
+
}
|
|
2196
|
+
if (sessions.length === 0) {
|
|
2197
|
+
output += "No conversations yet.\n";
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
content: [{ type: "text", text: output }]
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2105
2205
|
// src/index.ts
|
|
2106
2206
|
var apiKey = process.env.EASYFUNNEL_API_KEY;
|
|
2107
2207
|
if (!apiKey) {
|
|
@@ -2132,7 +2232,8 @@ server.setRequestHandler(import_types.ListToolsRequestSchema, async () => ({
|
|
|
2132
2232
|
queryEventsDefinition,
|
|
2133
2233
|
deleteFunnelDefinition,
|
|
2134
2234
|
updateFunnelDefinition,
|
|
2135
|
-
setupChatWidgetDefinition
|
|
2235
|
+
setupChatWidgetDefinition,
|
|
2236
|
+
queryConversationsDefinition
|
|
2136
2237
|
]
|
|
2137
2238
|
}));
|
|
2138
2239
|
server.setRequestHandler(import_types.CallToolRequestSchema, async (request) => {
|
|
@@ -2168,6 +2269,8 @@ server.setRequestHandler(import_types.CallToolRequestSchema, async (request) =>
|
|
|
2168
2269
|
return updateFunnel(client, args);
|
|
2169
2270
|
case "setup_chat_widget":
|
|
2170
2271
|
return setupChatWidget(client, args);
|
|
2272
|
+
case "query_conversations":
|
|
2273
|
+
return queryConversations(client, args);
|
|
2171
2274
|
default:
|
|
2172
2275
|
throw new Error(`Unknown tool: ${name}`);
|
|
2173
2276
|
}
|