saasfunnels 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,9 +37,9 @@ saasfunnels plans handoff --integration-id <id> \
37
37
  prices. Files that reference a Stripe price are reported as evidence; files that
38
38
  only look commercial by name and content are reported separately as guesses.
39
39
  Nothing leaves the machine until `.saasfunnels/plan-sources.json` lists the files
40
- and `plans handoff --send` is run, because plan mapping receives file contents
41
- rather than the `file:line` references a feature manifest sends. Commit that file
42
- so the approved set is reviewable.
40
+ and `plans handoff --send` is run. The CLI extracts normalized mapping evidence
41
+ locally; source contents stay on your machine. Commit the approval file so the
42
+ approved set is reviewable.
43
43
 
44
44
  ### GitHub Action
45
45
 
@@ -75,6 +75,10 @@ The application imports the exact versioned MCP registry through the server-side
75
75
 
76
76
  In hosted OAuth, `list_workspaces` discovers accessible workspace names and IDs. Live read, inspect, and plan tools accept an optional `workspace_id`. The hosted application must select the sole accessible workspace automatically, require an explicit ID when several are accessible, recheck authorization for every call, and include the selected workspace name and ID in each live response. The application passes the selection through to its developer API as the `workspace_id` query parameter. Local stdio API-key tools do not expose workspace selection; their key stays pinned to its workspace.
77
77
 
78
+ For Funnel discovery, hosted MCP offers `list_funnels` and `get_funnel_state`. `list_funnels` returns at most 50 summaries per page; follow `page.hasMore` with the next `offset` to cover the workspace. The app returns the selected workspace and evaluation time with each read. `get_funnel_state` reports the current lifecycle and publication readiness for one Funnel. These reads do not include Funnel definitions, customer data, or Results. Signals remain supporting evidence and enrollment inputs.
79
+
80
+ For performance, `get_portfolio_results` and `get_funnel_results` use a 7d, 30d, 90d, or 12m reporting window (30d by default). Portfolio rows are paged at 50 Funnels and cover at most the first 100; check `page.has_more` and `funnels_truncated` before claiming complete coverage. Stage counts and revenue rows retain their distinct attribution, value basis, and currency. `list_opportunities` and `list_leads` return at most 50 records per page with `page.total_count` and `page.has_more`; `get_opportunity` and `get_lead` return one bounded record. Opportunity reads omit private evidence and money; Lead reads omit personal identity and raw responses. These six reads require hosted OAuth, an accessible selected workspace, and the app's `funnels.view` role check.
81
+
78
82
  ## Development
79
83
 
80
84
  ```bash
@@ -87,3 +91,13 @@ The package allowlist is enforced as `LICENSE`, `README.md`, `package.json`, the
87
91
  Documentation: https://docs.saasfunnels.ai/developer-tools/saasfunnels-cli
88
92
 
89
93
  Support: support@saasfunnels.ai
94
+
95
+ ### Guided application discovery
96
+
97
+ Use `saasfunnels setup run` from the application repository after creating app credentials in Setup. Load `SAASFUNNELS_API_KEY` from **Developer setup access** into the local process environment. Its scopes are `developer:read` and `features:write`; it is not a browser or payment-administration key.
98
+
99
+ The command asks before uploading structured discovery proposals. `--send` provides explicit non-interactive approval. It coordinates features, pricing extraction, and plan branches, reports progress, and waits for the Stripe catalog. Run `saasfunnels setup run --resume` after an interruption. Existing accepted stages and customer review decisions are preserved.
100
+
101
+ If extraction is unsupported, review a `.saasfunnels/setup-pricing.json` file with `{ "plans": [{ "key": "pro", "name": "Pro", "features": { "export": true }, "prices": [{ "key": "price_ACTUAL_ID" }] }] }`, then resume. Use real identifiers from the connected merchant. Raw source files are never uploaded by guided setup or the pricing handoff.
102
+
103
+ The command does not implement arbitrary payment code, approve feature access, or verify transactions. Complete **Review and verify** in Setup after deploying the runtime and payment integration. Requires the server's guided setup API release.
package/dist/library.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createRequire as __saasfunnelsCreateRequire } from "node:module"; const require = __saasfunnelsCreateRequire(import.meta.url);
1
+ import { createRequire as __saasfunnelsCreateRequire } from "node:module"; const require = __saasfunnelsCreateRequire(import.meta.url); const __filename = new URL(import.meta.url).pathname; const __dirname = require("node:path").dirname(__filename);
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -24151,6 +24151,10 @@ var registry2 = Object.freeze({
24151
24151
  "local_mcp",
24152
24152
  "api_key"
24153
24153
  ]),
24154
+ "funnel.workspace.read": capability("funnel.workspace.read", "read", [
24155
+ "in_app",
24156
+ "hosted_mcp"
24157
+ ]),
24154
24158
  "setup.test_event.send": capability("setup.test_event.send", "external_write", [
24155
24159
  "local_cli",
24156
24160
  "local_mcp",
@@ -24260,7 +24264,7 @@ function validateFeatureCatalogManifest(value) {
24260
24264
 
24261
24265
  // src/identity.ts
24262
24266
  var SAASFUNNELS_CLI_NAME = "saasfunnels";
24263
- var SAASFUNNELS_CLI_VERSION = "0.2.2";
24267
+ var SAASFUNNELS_CLI_VERSION = "0.3.0";
24264
24268
  var SAASFUNNELS_PRODUCT_NAME = "SaaSFunnels";
24265
24269
  var SAASFUNNELS_DEFAULT_API_BASE_URL = "https://app.saasfunnels.ai";
24266
24270
  var SAASFUNNELS_ENV = Object.freeze({
@@ -24297,11 +24301,19 @@ var saasFunnelsMcpToolCapabilityIds = {
24297
24301
  apply_funnel_entry: "funnel.entry.apply",
24298
24302
  generate_direct_api_handoff: "developer.handoff.generate",
24299
24303
  get_integration_health: "developer.workspace.read",
24304
+ get_funnel_results: "funnel.workspace.read",
24305
+ get_funnel_state: "funnel.workspace.read",
24306
+ get_lead: "funnel.workspace.read",
24307
+ get_opportunity: "funnel.workspace.read",
24308
+ get_portfolio_results: "funnel.workspace.read",
24300
24309
  get_signal_payload_preview: "developer.workspace.read",
24301
24310
  get_workspace_readiness: "developer.workspace.read",
24302
24311
  list_workspaces: "developer.workspace.read",
24303
24312
  inspect_funnel_entry: "funnel.entry.inspect",
24313
+ list_funnels: "funnel.workspace.read",
24314
+ list_leads: "funnel.workspace.read",
24304
24315
  list_mapping_gaps: "developer.workspace.read",
24316
+ list_opportunities: "funnel.workspace.read",
24305
24317
  list_recent_signals: "developer.workspace.read",
24306
24318
  simulate_account_funnel_fit: "account.funnel_fit.simulate",
24307
24319
  propose_funnel_entry_installation: "funnel.entry.plan",
@@ -24380,6 +24392,42 @@ function stringArg(args, name) {
24380
24392
  const value = args[name];
24381
24393
  return typeof value === "string" && value.trim() ? value.trim() : void 0;
24382
24394
  }
24395
+ var uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
24396
+ var resultsWindows = ["7d", "30d", "90d", "12m"];
24397
+ var attributionRoles = ["direct", "influenced", "observed"];
24398
+ var opportunityStates = ["detected", "open", "eligible", "queued", "presented", "engaged", "action_pending", "converted", "dismissed", "suppressed", "expired", "failed"];
24399
+ var leadStates = ["new", "working", "qualified", "converted", "disqualified", "withdrawn", "deleted"];
24400
+ function uuidArg(args, name, required2 = false) {
24401
+ const value = args[name];
24402
+ if (value === void 0 && !required2) return void 0;
24403
+ const parsed = stringArg(args, name);
24404
+ if (!parsed || !uuidPattern.test(parsed)) {
24405
+ throw new McpProtocolError(-32602, `${name} must be a UUID.`);
24406
+ }
24407
+ return parsed;
24408
+ }
24409
+ function integerArg(args, name, fallback, max) {
24410
+ const value = args[name] ?? fallback;
24411
+ if (!Number.isInteger(value) || Number(value) < (name === "limit" ? 1 : 0) || Number(value) > max) {
24412
+ throw new McpProtocolError(-32602, `${name} must be an integer from ${name === "limit" ? 1 : 0} to ${max}.`);
24413
+ }
24414
+ return Number(value);
24415
+ }
24416
+ function optionalFilterArg(args, name) {
24417
+ if (args[name] === void 0) return void 0;
24418
+ const value = stringArg(args, name);
24419
+ if (!value || value.length > 80) {
24420
+ throw new McpProtocolError(-32602, `${name} must be a non-empty string of at most 80 characters.`);
24421
+ }
24422
+ return value;
24423
+ }
24424
+ function resultsWindowArg(args) {
24425
+ const value = args.window ?? "30d";
24426
+ if (typeof value !== "string" || !resultsWindows.includes(value)) {
24427
+ throw new McpProtocolError(-32602, "window must be 7d, 30d, 90d, or 12m.");
24428
+ }
24429
+ return value;
24430
+ }
24383
24431
  function sourceArg(args) {
24384
24432
  const source = stringArg(args, "source") ?? "direct";
24385
24433
  if (!sourceNames.has(source)) {
@@ -24671,11 +24719,19 @@ function emptyLiveInputSchema() {
24671
24719
  }
24672
24720
  var hostedWorkspaceToolNames = /* @__PURE__ */ new Set([
24673
24721
  "get_integration_health",
24722
+ "get_funnel_results",
24723
+ "get_funnel_state",
24724
+ "get_lead",
24725
+ "get_opportunity",
24726
+ "get_portfolio_results",
24674
24727
  "get_workspace_readiness",
24675
24728
  "list_mapping_gaps",
24676
24729
  "list_recent_signals",
24677
24730
  "get_signal_payload_preview",
24678
24731
  "inspect_funnel_entry",
24732
+ "list_funnels",
24733
+ "list_leads",
24734
+ "list_opportunities",
24679
24735
  "propose_funnel_entry_installation",
24680
24736
  "get_account_strategy",
24681
24737
  "simulate_account_funnel_fit"
@@ -24879,6 +24935,136 @@ function saasFunnelsMcpToolDefinitions({
24879
24935
  }
24880
24936
  ];
24881
24937
  if (includeHostedAccountTools) {
24938
+ const readAnnotations = {
24939
+ idempotentHint: true,
24940
+ openWorldHint: true,
24941
+ readOnlyHint: true
24942
+ };
24943
+ const windowSchema = { default: "30d", enum: resultsWindows, type: "string" };
24944
+ const uuidSchema = { format: "uuid", type: "string" };
24945
+ tools.push({
24946
+ annotations: readAnnotations,
24947
+ description: "Read bounded portfolio Results for one workspace and reporting window (30d by default). Funnel rows are paged within the first 100; stage counts, acquisition aggregates, and revenue value bases stay distinct. Check page.has_more and funnels_truncated for coverage.",
24948
+ inputSchema: {
24949
+ additionalProperties: false,
24950
+ properties: {
24951
+ attribution_role: { enum: attributionRoles, type: "string" },
24952
+ funnel_id: uuidSchema,
24953
+ limit: { type: "integer", minimum: 1, maximum: 50 },
24954
+ objective: { enum: ["convert", "grow", "save"], type: "string" },
24955
+ offset: { type: "integer", minimum: 0, maximum: 100 },
24956
+ window: windowSchema
24957
+ },
24958
+ type: "object"
24959
+ },
24960
+ name: "get_portfolio_results",
24961
+ title: "Get Portfolio Results"
24962
+ });
24963
+ tools.push({
24964
+ annotations: readAnnotations,
24965
+ description: "Read one Funnel's Results for a reporting window, including stage counts, configured acquisition aggregates, maturity, warnings, and separate revenue rows. Bounded arrays carry truncation flags.",
24966
+ inputSchema: {
24967
+ additionalProperties: false,
24968
+ properties: { funnel_id: uuidSchema, window: windowSchema },
24969
+ required: ["funnel_id"],
24970
+ type: "object"
24971
+ },
24972
+ name: "get_funnel_results",
24973
+ title: "Get Funnel Results"
24974
+ });
24975
+ tools.push({
24976
+ annotations: readAnnotations,
24977
+ description: "List up to 50 Opportunities in one workspace, optionally filtered by Funnel or lifecycle state. Returns exact total count and page.has_more; no raw evidence or monetary values.",
24978
+ inputSchema: {
24979
+ additionalProperties: false,
24980
+ properties: {
24981
+ funnel_id: uuidSchema,
24982
+ limit: { type: "integer", minimum: 1, maximum: 50 },
24983
+ offset: { type: "integer", minimum: 0, maximum: 25e4 },
24984
+ state: { enum: opportunityStates, type: "string" }
24985
+ },
24986
+ type: "object"
24987
+ },
24988
+ name: "list_opportunities",
24989
+ title: "List Opportunities"
24990
+ });
24991
+ tools.push({
24992
+ annotations: readAnnotations,
24993
+ description: "Read one Opportunity's bounded Funnel, account, lifecycle, delivery, and outcome summary in the selected workspace. No private evidence or monetary values.",
24994
+ inputSchema: {
24995
+ additionalProperties: false,
24996
+ properties: { opportunity_id: uuidSchema },
24997
+ required: ["opportunity_id"],
24998
+ type: "object"
24999
+ },
25000
+ name: "get_opportunity",
25001
+ title: "Get Opportunity"
25002
+ });
25003
+ tools.push({
25004
+ annotations: readAnnotations,
25005
+ description: "List up to 50 generated Leads in one workspace with source Funnel, lifecycle, delivery, and capture counts. Returns exact total count and page.has_more; no personal identity or raw responses.",
25006
+ inputSchema: {
25007
+ additionalProperties: false,
25008
+ properties: {
25009
+ funnel_id: uuidSchema,
25010
+ limit: { type: "integer", minimum: 1, maximum: 50 },
25011
+ offset: { type: "integer", minimum: 0, maximum: 25e4 },
25012
+ state: { enum: leadStates, type: "string" }
25013
+ },
25014
+ type: "object"
25015
+ },
25016
+ name: "list_leads",
25017
+ title: "List Leads"
25018
+ });
25019
+ tools.push({
25020
+ annotations: readAnnotations,
25021
+ description: "Read one generated Lead's bounded source, lifecycle, delivery, capture, and verified outcome summary in the selected workspace. No personal identity or raw responses.",
25022
+ inputSchema: {
25023
+ additionalProperties: false,
25024
+ properties: { lead_id: uuidSchema },
25025
+ required: ["lead_id"],
25026
+ type: "object"
25027
+ },
25028
+ name: "get_lead",
25029
+ title: "Get Lead"
25030
+ });
25031
+ tools.push({
25032
+ annotations: {
25033
+ idempotentHint: true,
25034
+ openWorldHint: true,
25035
+ readOnlyHint: true
25036
+ },
25037
+ description: "List one selected workspace's Funnels with lifecycle, draft, and publication state. Results include evaluation time, total count, and pagination; use offset to read more pages. No Funnel definitions or customer data.",
25038
+ inputSchema: {
25039
+ additionalProperties: false,
25040
+ properties: {
25041
+ include_archived: { type: "boolean" },
25042
+ limit: { type: "integer", minimum: 1, maximum: 50 },
25043
+ offset: { type: "integer", minimum: 0 }
25044
+ },
25045
+ type: "object"
25046
+ },
25047
+ name: "list_funnels",
25048
+ title: "List Funnels"
25049
+ });
25050
+ tools.push({
25051
+ annotations: {
25052
+ idempotentHint: true,
25053
+ openWorldHint: true,
25054
+ readOnlyHint: true
25055
+ },
25056
+ description: "Read one Funnel's current lifecycle, draft and published revisions, and server-derived publication readiness in the selected workspace. No definitions or private evidence.",
25057
+ inputSchema: {
25058
+ additionalProperties: false,
25059
+ properties: {
25060
+ funnel_id: { type: "string", format: "uuid" }
25061
+ },
25062
+ required: ["funnel_id"],
25063
+ type: "object"
25064
+ },
25065
+ name: "get_funnel_state",
25066
+ title: "Get Funnel State"
25067
+ });
24882
25068
  tools.push({
24883
25069
  annotations: {
24884
25070
  idempotentHint: true,
@@ -25210,7 +25396,7 @@ async function callSaaSFunnelsMcpTool(name, args, options = {}) {
25210
25396
  "workspace_id is available only with hosted OAuth; an API key is pinned to its workspace."
25211
25397
  );
25212
25398
  }
25213
- if ((name === "get_account_strategy" || name === "simulate_account_funnel_fit" || name === "list_workspaces") && !options.hostedMode) {
25399
+ if ((name === "get_account_strategy" || name === "get_funnel_results" || name === "get_funnel_state" || name === "get_lead" || name === "get_opportunity" || name === "get_portfolio_results" || name === "list_funnels" || name === "list_leads" || name === "list_opportunities" || name === "simulate_account_funnel_fit" || name === "list_workspaces") && !options.hostedMode) {
25214
25400
  throw new McpToolExecutionError(
25215
25401
  "This tool requires hosted OAuth.",
25216
25402
  {
@@ -25261,6 +25447,102 @@ async function callSaaSFunnelsMcpTool(name, args, options = {}) {
25261
25447
  return structuredToolResult(
25262
25448
  await developerGet("/api/developer-tools/workspaces", {}, options)
25263
25449
  );
25450
+ if (name === "list_funnels") {
25451
+ const limit = args.limit ?? 25;
25452
+ const offset = args.offset ?? 0;
25453
+ if (!Number.isInteger(limit) || Number(limit) < 1 || Number(limit) > 50 || !Number.isInteger(offset) || Number(offset) < 0 || Number(offset) > 25e4 || args.include_archived !== void 0 && typeof args.include_archived !== "boolean") {
25454
+ throw new McpProtocolError(
25455
+ -32602,
25456
+ "limit must be 1\u201350, offset must be 0\u2013250000, and include_archived must be boolean."
25457
+ );
25458
+ }
25459
+ const query = new URLSearchParams({
25460
+ include_archived: String(args.include_archived === true),
25461
+ limit: String(limit),
25462
+ offset: String(offset)
25463
+ });
25464
+ return structuredToolResult(
25465
+ await developerGet(`/api/developer-tools/funnels?${query}`, args, options)
25466
+ );
25467
+ }
25468
+ if (name === "get_funnel_state") {
25469
+ const funnelId = uuidArg(args, "funnel_id", true);
25470
+ return structuredToolResult(
25471
+ await developerGet(
25472
+ `/api/developer-tools/funnels/${encodeURIComponent(funnelId)}`,
25473
+ args,
25474
+ options
25475
+ )
25476
+ );
25477
+ }
25478
+ if (name === "get_portfolio_results") {
25479
+ const query = new URLSearchParams({
25480
+ window: resultsWindowArg(args),
25481
+ limit: String(integerArg(args, "limit", 50, 50)),
25482
+ offset: String(integerArg(args, "offset", 0, 100))
25483
+ });
25484
+ const funnelId = uuidArg(args, "funnel_id");
25485
+ if (funnelId) query.set("funnel_id", funnelId);
25486
+ const objective = optionalFilterArg(args, "objective");
25487
+ if (objective) {
25488
+ if (!["convert", "grow", "save"].includes(objective)) {
25489
+ throw new McpProtocolError(-32602, "objective must be convert, grow, or save.");
25490
+ }
25491
+ query.set("objective", objective);
25492
+ }
25493
+ const attributionRole = optionalFilterArg(args, "attribution_role");
25494
+ if (attributionRole) {
25495
+ if (!attributionRoles.includes(attributionRole)) {
25496
+ throw new McpProtocolError(-32602, "attribution_role must be direct, influenced, or observed.");
25497
+ }
25498
+ query.set("attribution_role", attributionRole);
25499
+ }
25500
+ return structuredToolResult(
25501
+ await developerGet(`/api/developer-tools/results?${query}`, args, options)
25502
+ );
25503
+ }
25504
+ if (name === "get_funnel_results") {
25505
+ const funnelId = uuidArg(args, "funnel_id", true);
25506
+ const query = new URLSearchParams({ window: resultsWindowArg(args) });
25507
+ return structuredToolResult(
25508
+ await developerGet(
25509
+ `/api/developer-tools/funnels/${encodeURIComponent(funnelId)}/results?${query}`,
25510
+ args,
25511
+ options
25512
+ )
25513
+ );
25514
+ }
25515
+ if (name === "list_opportunities" || name === "list_leads") {
25516
+ const query = new URLSearchParams({
25517
+ limit: String(integerArg(args, "limit", 25, 50)),
25518
+ offset: String(integerArg(args, "offset", 0, 25e4))
25519
+ });
25520
+ const funnelId = uuidArg(args, "funnel_id");
25521
+ if (funnelId) query.set("funnel_id", funnelId);
25522
+ const state = optionalFilterArg(args, "state");
25523
+ if (state) {
25524
+ const allowed = name === "list_leads" ? leadStates : opportunityStates;
25525
+ if (!allowed.includes(state)) {
25526
+ throw new McpProtocolError(-32602, `state is not a supported ${name === "list_leads" ? "Lead" : "Opportunity"} state.`);
25527
+ }
25528
+ query.set("state", state);
25529
+ }
25530
+ const resource = name === "list_leads" ? "leads" : "opportunities";
25531
+ return structuredToolResult(
25532
+ await developerGet(`/api/developer-tools/${resource}?${query}`, args, options)
25533
+ );
25534
+ }
25535
+ if (name === "get_opportunity" || name === "get_lead") {
25536
+ const resource = name === "get_lead" ? "leads" : "opportunities";
25537
+ const id = uuidArg(args, name === "get_lead" ? "lead_id" : "opportunity_id", true);
25538
+ return structuredToolResult(
25539
+ await developerGet(
25540
+ `/api/developer-tools/${resource}/${encodeURIComponent(id)}`,
25541
+ args,
25542
+ options
25543
+ )
25544
+ );
25545
+ }
25264
25546
  if (name === "get_workspace_readiness")
25265
25547
  return structuredToolResult(
25266
25548
  await developerGet(