@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.
Files changed (2) hide show
  1. package/dist/index.js +150 -47
  2. 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: "process.env.NEXT_PUBLIC_EASYFUNNEL_KEY!",
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: "import.meta.env.VITE_EASYFUNNEL_KEY",
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: "process.env.REACT_APP_EASYFUNNEL_KEY!",
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: "import.meta.env.PUBLIC_EASYFUNNEL_KEY",
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
- IMPORTANT: Restart your dev server for the env var to take effect.
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: After restarting, I'll verify everything works with a test event.`;
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 candidates = [
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
- if (envResult.value && envResult.value.startsWith("ef_")) {
1669
- checks.push({
1670
- name: `${envResult.file} contains ${envResult.varName}`,
1671
- passed: true,
1672
- detail: `${envResult.varName}=${envResult.value.slice(0, 8)}...`
1673
- });
1674
- } else if (envResult.value) {
1675
- checks.push({
1676
- name: `${envResult.file} contains ${envResult.varName}`,
1677
- passed: false,
1678
- detail: `Value doesn't start with "ef_". Got: ${envResult.value.slice(0, 20)}`
1679
- });
1680
- } else {
1681
- checks.push({
1682
- name: `${envResult.file} contains ${envResult.varName}`,
1683
- passed: false,
1684
- detail: `${envResult.varName} is empty`
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: "Env file contains API key",
1712
+ name: "API key configured",
1690
1713
  passed: false,
1691
- detail: "No EasyFunnel API key found in .env.local or .env"
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("Env file") || check.name.includes("API key")) {
1793
- const varName = envResult?.varName || "NEXT_PUBLIC_EASYFUNNEL_KEY";
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 += ` ${varName}=ef_your_key_here
1818
+ output += `Alternatively, add it to an env file:
1798
1819
  `;
1799
- output += `Then restart your dev server.
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyfunnel/mcp",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "MCP server for easyfunnel.co — AI-powered analytics tools for Claude/Cursor",
5
5
  "main": "dist/index.js",
6
6
  "bin": {