@myna-sh/mcp 0.16.1 → 0.17.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
@@ -1,6 +1,6 @@
1
1
  # @myna-sh/mcp
2
2
 
3
- Model Context Protocol server for operating [Myna](https://myna.sh) content and feedback from coding agents and MCP clients.
3
+ Model Context Protocol server for operating [Myna](https://myna.sh) content, analytics, and feedback from coding agents and MCP clients.
4
4
 
5
5
  > **Connecting Claude, ChatGPT, or another remote agent?** You do not need this package. Myna hosts the same server at `https://api.myna.sh/mcp` — add it as a custom connector and authorize it with OAuth. See [connecting a remote agent](https://docs.myna.sh/mcp/hosted). This package is for local coding agents that run the server over stdio.
6
6
 
package/dist/main.d.ts CHANGED
@@ -97,8 +97,8 @@ interface ToolOptions {
97
97
  * conservative default, and the one that matters: a customer's saved agent
98
98
  * configuration must not change shape because Myna shipped a second product.
99
99
  * There are already 45+ tools on this server, and every one added degrades
100
- * selection for all of them, so a project that does not run Feedback should
101
- * never be shown Feedback's.
100
+ * selection for all of them, so a project that does not run a product should
101
+ * never be shown that product's tools.
102
102
  */
103
103
  products?: readonly string[];
104
104
  }
package/dist/main.js CHANGED
@@ -771,6 +771,110 @@ var ManagementClient = class {
771
771
  revokeForProject: (project, key) => this.mutate("DELETE", `/projects/${enc(project)}/api-keys/${enc(key)}`)
772
772
  };
773
773
  // --- Webhooks -------------------------------------------------------------
774
+ /**
775
+ * Myna Analytics.
776
+ *
777
+ * Shaped for a caller that has to *reason* about the numbers rather than draw
778
+ * them. Every read echoes the window it resolved, `overview` carries the
779
+ * previous period and the movement, and `releaseImpact` answers "what
780
+ * happened after we shipped this" in one call instead of six — which for an
781
+ * agent is one turn instead of six.
782
+ */
783
+ analytics = {
784
+ /**
785
+ * Headline numbers and how they moved.
786
+ *
787
+ * `period` is `24h`, `7d`, `30d`, `12w`; `from`/`to` is the exact form.
788
+ * Naming both is an error rather than a precedence rule, because a silent
789
+ * guess about which window produced a number is indistinguishable from a
790
+ * correct answer.
791
+ */
792
+ overview: (project, query = {}, signal) => this.get(`/projects/${enc(project)}/analytics/overview`, query, signal),
793
+ /** All four metrics over time, bucketed by hour, day, or week. */
794
+ series: (project, query = {}, signal) => this.get(`/projects/${enc(project)}/analytics/series`, query, signal),
795
+ /**
796
+ * Group by a dimension, or by a custom property as `property:<key>`.
797
+ *
798
+ * `entry`, `collection`, and `release` are the dimensions no general
799
+ * analytics product has, because they name things in the CMS on the other
800
+ * side of the same project.
801
+ */
802
+ breakdown: (project, query, signal) => this.get(`/projects/${enc(project)}/analytics/breakdown`, query, signal),
803
+ /**
804
+ * Conversion through ordered steps.
805
+ *
806
+ * A POST because a funnel definition does not fit in a query string, not
807
+ * because it writes anything. Order is enforced and `withinHours` is
808
+ * measured from the visitor's first step.
809
+ */
810
+ funnel: (project, body, signal) => this.http.request("POST", `/projects/${enc(project)}/analytics/funnel`, {
811
+ body,
812
+ signal
813
+ }),
814
+ /** Where people stop: exits, bounces, and pages that generate complaints. */
815
+ friction: (project, query = {}, signal) => this.get(`/projects/${enc(project)}/analytics/friction`, query, signal),
816
+ /**
817
+ * Every event name this project has recorded, and what each one means.
818
+ *
819
+ * Read this before writing a funnel: `description` is where somebody wrote
820
+ * down whether `checkout_started` fires on the click or on the page, which
821
+ * nothing in the data itself says.
822
+ */
823
+ events: (project, query = {}, signal) => this.get(`/projects/${enc(project)}/analytics/events`, query, signal),
824
+ event: (project, event, query = {}, signal) => this.get(
825
+ `/projects/${enc(project)}/analytics/events/${enc(event)}`,
826
+ query,
827
+ signal
828
+ ),
829
+ /** Write down what an event means. `null` clears it. */
830
+ describeEvent: (project, event, body) => this.mutate(
831
+ "PATCH",
832
+ `/projects/${enc(project)}/analytics/events/${enc(event)}`,
833
+ body
834
+ ),
835
+ /** Traffic and complaints per content entry. */
836
+ content: (project, query = {}, signal) => this.get(`/projects/${enc(project)}/analytics/content`, query, signal),
837
+ /** One entry: its numbers, every publication in the window, and their effect. */
838
+ entry: (project, entry, query = {}, signal) => this.get(
839
+ `/projects/${enc(project)}/analytics/content/${enc(entry)}`,
840
+ query,
841
+ signal
842
+ ),
843
+ /**
844
+ * What measurably happened after a release.
845
+ *
846
+ * Correlation, and it says so: a release is not the only thing that
847
+ * happened that day. What it removes is every reason not to look.
848
+ */
849
+ releaseImpact: (project, release, opts = {}, signal) => this.get(
850
+ `/projects/${enc(project)}/analytics/releases/${release}/impact`,
851
+ opts,
852
+ signal
853
+ ),
854
+ /** People the customer's own backend vouched for. Anonymous visitors are not here. */
855
+ profiles: (project, opts = {}) => {
856
+ const { q, ...rest } = opts;
857
+ return this.page(`/projects/${enc(project)}/analytics/profiles`, rest, { q });
858
+ },
859
+ /** By Myna id (`apr_…`) or by the customer's own user id. */
860
+ profile: (project, profile, signal) => this.get(
861
+ `/projects/${enc(project)}/analytics/profiles/${enc(profile)}`,
862
+ void 0,
863
+ signal
864
+ ),
865
+ sessions: (project, opts = {}) => {
866
+ const { profile, ...rest } = opts;
867
+ return this.page(`/projects/${enc(project)}/analytics/sessions`, rest, { profile });
868
+ },
869
+ session: (project, session, signal) => this.get(
870
+ `/projects/${enc(project)}/analytics/sessions/${enc(session)}`,
871
+ void 0,
872
+ signal
873
+ ),
874
+ settings: (project, signal) => this.get(`/projects/${enc(project)}/analytics/settings`, void 0, signal),
875
+ /** Retention and path exclusions. Shortening retention deletes data. */
876
+ updateSettings: (project, body) => this.mutate("PUT", `/projects/${enc(project)}/analytics/settings`, body)
877
+ };
774
878
  webhooks = {
775
879
  list: (project, signal) => this.get(`/projects/${enc(project)}/webhooks`, void 0, signal),
776
880
  create: (project, body) => this.mutate("POST", `/projects/${enc(project)}/webhooks`, body),
@@ -1087,7 +1191,17 @@ var TOOL_CAPABILITY = {
1087
1191
  myna_request_retest: "feedback:write",
1088
1192
  myna_merge_report: "feedback:write",
1089
1193
  myna_move_report: "feedback:write",
1090
- myna_block_reporter: "feedback:manage"
1194
+ myna_block_reporter: "feedback:manage",
1195
+ myna_analytics_overview: "analytics:read",
1196
+ myna_analytics_breakdown: "analytics:read",
1197
+ myna_analytics_events: "analytics:read",
1198
+ myna_analytics_funnel: "analytics:read",
1199
+ myna_analytics_friction: "analytics:read",
1200
+ myna_analytics_content: "analytics:read",
1201
+ myna_analytics_entry: "analytics:read",
1202
+ myna_analytics_release_impact: "analytics:read",
1203
+ myna_analytics_person: "analytics:read",
1204
+ myna_describe_analytics_event: "analytics:write"
1091
1205
  };
1092
1206
  function registerTools(server, registry, options = { allowPathUploads: true }) {
1093
1207
  server.registerTool(
@@ -2160,6 +2274,7 @@ function registerTools(server, registry, options = { allowPathUploads: true }) {
2160
2274
  }
2161
2275
  );
2162
2276
  if (options.products?.includes("feedback")) registerFeedbackTools(server, registry);
2277
+ if (options.products?.includes("analytics")) registerAnalyticsTools(server, registry);
2163
2278
  }
2164
2279
  var reportArg = {
2165
2280
  ...projectArg,
@@ -2545,6 +2660,290 @@ function registerFeedbackTools(server, registry) {
2545
2660
  }
2546
2661
  );
2547
2662
  }
2663
+ var analyticsFilterArgs = {
2664
+ ...projectArg,
2665
+ period: z.string().optional().describe("Window: 24h, 7d, 30d, 12w. Defaults to 7d. Do not combine with from/to."),
2666
+ from: z.string().optional().describe("ISO start of an explicit window."),
2667
+ to: z.string().optional().describe("ISO end of an explicit window."),
2668
+ event: z.string().optional().describe("Only this event name."),
2669
+ path: z.string().optional().describe("Only this page path."),
2670
+ referrerDomain: z.string().optional().describe("Only traffic from this referrer host."),
2671
+ utmSource: z.string().optional(),
2672
+ utmMedium: z.string().optional(),
2673
+ utmCampaign: z.string().optional(),
2674
+ browser: z.string().optional(),
2675
+ os: z.string().optional(),
2676
+ deviceType: z.string().optional().describe("desktop, mobile, or tablet."),
2677
+ locale: z.string().optional(),
2678
+ environment: z.string().optional().describe("The application's own environment name."),
2679
+ appVersion: z.string().optional().describe("The application's own build."),
2680
+ entry: z.string().optional().describe("Only events on this content entry (ent_...)."),
2681
+ collection: z.string().optional().describe("Only events on entries in this collection."),
2682
+ release: z.number().optional().describe("Only events on this Myna release."),
2683
+ property: z.string().optional().describe("`key:value` against an event's custom properties."),
2684
+ identified: z.enum(["true", "false"]).optional().describe("true for people the customer's backend vouched for, false for everyone else.")
2685
+ };
2686
+ function analyticsQueryOf(args) {
2687
+ const { project: _project, ...rest } = args;
2688
+ return rest;
2689
+ }
2690
+ function registerAnalyticsTools(server, registry) {
2691
+ server.registerTool(
2692
+ "myna_analytics_overview",
2693
+ {
2694
+ title: "Analytics overview",
2695
+ description: "Headline product usage for a window, with the previous period beside it and the percentage change: visitors, sessions, page views, events, bounce rate, session length, top pages, top events, top referrers, and what share of visitors are identified. Start here \u2014 it answers 'is anything different' in one call. Every filter narrows every number.",
2696
+ inputSchema: { ...analyticsFilterArgs },
2697
+ annotations: { readOnlyHint: true, openWorldHint: true }
2698
+ },
2699
+ async (args) => {
2700
+ try {
2701
+ const { client, project } = registry.clientFor(args.project);
2702
+ const result = await client.analytics.overview(project, analyticsQueryOf(args));
2703
+ const summary = [
2704
+ `${result.totals.visitors} visitors (${result.change.visitors ?? "\u2014"}%)`,
2705
+ `${result.totals.sessions} sessions`,
2706
+ `${result.totals.pageviews} page views (${result.change.pageviews ?? "\u2014"}%)`,
2707
+ `bounce ${result.averages.bounceRate}%`
2708
+ ].join(", ");
2709
+ return ok(summary, result);
2710
+ } catch (error) {
2711
+ return fail(error);
2712
+ }
2713
+ }
2714
+ );
2715
+ server.registerTool(
2716
+ "myna_analytics_breakdown",
2717
+ {
2718
+ title: "Break analytics down by a dimension",
2719
+ description: "Group events by a dimension and get visitors, sessions, page views, and events for each. Built-in dimensions: event, path, referrerDomain, utmSource, utmMedium, utmCampaign, browser, os, deviceType, locale, environment, appVersion, entry, collection, release. A custom event property is `property:<key>` \u2014 call myna_analytics_events first to see which keys exist. `entry`, `collection`, and `release` name Myna content, which is what makes 'which pages did this release change, and did their traffic move' answerable at all.",
2720
+ inputSchema: {
2721
+ ...analyticsFilterArgs,
2722
+ dimension: z.string().describe("What to group by. See the description."),
2723
+ metric: z.enum(["visitors", "sessions", "pageviews", "events"]).optional().describe("What to sort and compute shares by. Defaults to visitors."),
2724
+ limit: z.number().optional().describe("Rows to return, up to 100. Defaults to 25.")
2725
+ },
2726
+ annotations: { readOnlyHint: true, openWorldHint: true }
2727
+ },
2728
+ async (args) => {
2729
+ try {
2730
+ const { client, project } = registry.clientFor(args.project);
2731
+ const result = await client.analytics.breakdown(project, {
2732
+ ...analyticsQueryOf(args),
2733
+ dimension: args.dimension,
2734
+ metric: args.metric,
2735
+ limit: args.limit
2736
+ });
2737
+ return ok(`${result.rows.length} row(s) by ${result.dimension}.`, result);
2738
+ } catch (error) {
2739
+ return fail(error);
2740
+ }
2741
+ }
2742
+ );
2743
+ server.registerTool(
2744
+ "myna_analytics_events",
2745
+ {
2746
+ title: "What this project measures",
2747
+ description: "Every event name this project records, how often each fired in the window, which custom properties it carries, and \u2014 crucially \u2014 what somebody wrote down that it *means*. Read this before writing a funnel or a breakdown: nothing in the data says whether checkout_started fires on the click or on the page, and guessing wrong makes every number after it wrong. An event with a count of zero is an integration that stopped sending.",
2748
+ inputSchema: { ...analyticsFilterArgs },
2749
+ annotations: { readOnlyHint: true, openWorldHint: true }
2750
+ },
2751
+ async (args) => {
2752
+ try {
2753
+ const { client, project } = registry.clientFor(args.project);
2754
+ const events = await client.analytics.events(project, analyticsQueryOf(args));
2755
+ const undescribed = events.filter((e) => !e.description).length;
2756
+ return ok(
2757
+ `${events.length} event type(s)${undescribed > 0 ? `, ${undescribed} with no description` : ""}.`,
2758
+ { events }
2759
+ );
2760
+ } catch (error) {
2761
+ return fail(error);
2762
+ }
2763
+ }
2764
+ );
2765
+ server.registerTool(
2766
+ "myna_describe_analytics_event",
2767
+ {
2768
+ title: "Describe what an event means",
2769
+ description: "Write down what an event actually measures, so the next reader \u2014 a teammate or another agent \u2014 does not have to infer it from the name. This is the analytics equivalent of a collection's guidance. Say when it fires and what its properties mean; do not restate the name. Pass null to clear.",
2770
+ inputSchema: {
2771
+ ...projectArg,
2772
+ event: z.string().describe("The event name, exactly as it is recorded."),
2773
+ description: z.string().nullable().describe("What it means, or null to clear.")
2774
+ },
2775
+ annotations: { openWorldHint: true }
2776
+ },
2777
+ async (args) => {
2778
+ try {
2779
+ const { client, project } = registry.clientFor(args.project);
2780
+ const updated = await client.analytics.describeEvent(project, args.event, {
2781
+ description: args.description
2782
+ });
2783
+ return ok(`Described ${updated.name}.`, updated);
2784
+ } catch (error) {
2785
+ return fail(error);
2786
+ }
2787
+ }
2788
+ );
2789
+ server.registerTool(
2790
+ "myna_analytics_funnel",
2791
+ {
2792
+ title: "Conversion through ordered steps",
2793
+ description: "How many visitors got through a sequence of events, in order, within a time budget. Returns per step: visitors, conversion from the first and previous step, how many dropped, and the median seconds it took. Order is enforced \u2014 somebody who saw the confirmation page before starting checkout has not converted. Pin a step to a page with `path` when the same event fires in several places.",
2794
+ inputSchema: {
2795
+ ...projectArg,
2796
+ steps: z.array(
2797
+ z.object({
2798
+ event: z.string(),
2799
+ path: z.string().optional().describe("Narrow this step to one page."),
2800
+ label: z.string().optional()
2801
+ })
2802
+ ).describe("Two to eight steps, in the order a visitor performs them."),
2803
+ period: z.string().optional().describe("Window: 24h, 7d, 30d. Defaults to 7d."),
2804
+ from: z.string().optional(),
2805
+ to: z.string().optional(),
2806
+ withinHours: z.number().optional().describe("How long a visitor has to finish, measured from their first step. Defaults to 168.")
2807
+ },
2808
+ annotations: { readOnlyHint: true, openWorldHint: true }
2809
+ },
2810
+ async (args) => {
2811
+ try {
2812
+ const { client, project } = registry.clientFor(args.project);
2813
+ const result = await client.analytics.funnel(project, {
2814
+ steps: args.steps,
2815
+ period: args.period,
2816
+ from: args.from,
2817
+ to: args.to,
2818
+ withinHours: args.withinHours
2819
+ });
2820
+ const worst = [...result.steps].slice(1).sort((a, b) => a.conversionFromPrevious - b.conversionFromPrevious)[0];
2821
+ const summary = worst ? `${result.conversion}% end to end; the biggest drop is into "${worst.label}" at ${worst.conversionFromPrevious}%.` : `${result.conversion}% end to end.`;
2822
+ return ok(summary, result);
2823
+ } catch (error) {
2824
+ return fail(error);
2825
+ }
2826
+ }
2827
+ );
2828
+ server.registerTool(
2829
+ "myna_analytics_friction",
2830
+ {
2831
+ title: "Where people stop",
2832
+ description: "Three signals in one call: the pages visits most often end on with their exit rates, the pages people arrive at and leave without going anywhere else, and \u2014 the one no other analytics tool can compute \u2014 the pages that generate bug reports out of proportion to their traffic, with the reports themselves. Use this when asked why usage dropped and you do not yet know where to look. There are no rage clicks here: Myna does not read the DOM.",
2833
+ inputSchema: { ...analyticsFilterArgs },
2834
+ annotations: { readOnlyHint: true, openWorldHint: true }
2835
+ },
2836
+ async (args) => {
2837
+ try {
2838
+ const { client, project } = registry.clientFor(args.project);
2839
+ const result = await client.analytics.friction(project, analyticsQueryOf(args));
2840
+ const hotspot = result.feedbackHotspots[0];
2841
+ const summary = hotspot ? `Worst complaint rate: ${hotspot.path} at ${hotspot.reportsPerThousandSessions} reports per 1000 sessions.` : `${result.exits.length} exit page(s), ${result.bounces.length} bounce page(s).`;
2842
+ return ok(summary, result);
2843
+ } catch (error) {
2844
+ return fail(error);
2845
+ }
2846
+ }
2847
+ );
2848
+ server.registerTool(
2849
+ "myna_analytics_content",
2850
+ {
2851
+ title: "Traffic and complaints per content entry",
2852
+ description: "Which Myna content entries people actually reach, how much traffic each gets, and how many bug reports were filed from their pages. The report links behaviour to the entries you can edit \u2014 an entry with heavy traffic and a high complaint count is where a copy change pays. `unattributed` tells you what share of events carried no content context, which distinguishes 'this entry has no traffic' from 'this application never attaches content'.",
2853
+ inputSchema: { ...analyticsFilterArgs },
2854
+ annotations: { readOnlyHint: true, openWorldHint: true }
2855
+ },
2856
+ async (args) => {
2857
+ try {
2858
+ const { client, project } = registry.clientFor(args.project);
2859
+ const result = await client.analytics.content(project, analyticsQueryOf(args));
2860
+ return ok(
2861
+ `${result.entries.length} entr(ies) with traffic; ${result.unattributed.share}% of events carried no content context.`,
2862
+ result
2863
+ );
2864
+ } catch (error) {
2865
+ return fail(error);
2866
+ }
2867
+ }
2868
+ );
2869
+ server.registerTool(
2870
+ "myna_analytics_entry",
2871
+ {
2872
+ title: "One entry, its publications, and their effect",
2873
+ description: "Everything measured about one content entry: totals against the previous period, a daily series, which pages served it, its top events, every time it was published inside the window with the metric movement either side of each publication, and the reports filed from its pages. This is the call for 'did that copy change help' \u2014 it puts the edit and the numbers in one response.",
2874
+ inputSchema: { ...analyticsFilterArgs, entryId: z.string().describe("Entry id (ent_...).") },
2875
+ annotations: { readOnlyHint: true, openWorldHint: true }
2876
+ },
2877
+ async (args) => {
2878
+ try {
2879
+ const { client, project } = registry.clientFor(args.project);
2880
+ const { entryId, ...rest } = args;
2881
+ const result = await client.analytics.entry(project, entryId, analyticsQueryOf(rest));
2882
+ return ok(
2883
+ `${result.entry.title ?? entryId}: ${result.totals.visitors} visitors (${result.change.visitors ?? "\u2014"}%), ${result.publications.length} publication(s), ${result.relatedReports.length} related report(s).`,
2884
+ result
2885
+ );
2886
+ } catch (error) {
2887
+ return fail(error);
2888
+ }
2889
+ }
2890
+ );
2891
+ server.registerTool(
2892
+ "myna_analytics_release_impact",
2893
+ {
2894
+ title: "What happened after a release",
2895
+ description: "Compare equal windows either side of a published release: overall metrics, per entry the release touched, per event name, and the bug reports that came in afterwards. This is correlation, not causation \u2014 a release is not the only thing that happened that day \u2014 but it is the whole picture in one call, and it is the fastest way to find out whether a content change moved a number. The `after` window is clamped to now, so a release published yesterday is compared over one day rather than seven.",
2896
+ inputSchema: {
2897
+ ...projectArg,
2898
+ release: z.number().describe("The release number."),
2899
+ windowDays: z.number().optional().describe("Days either side. Defaults to 7, maximum 90.")
2900
+ },
2901
+ annotations: { readOnlyHint: true, openWorldHint: true }
2902
+ },
2903
+ async (args) => {
2904
+ try {
2905
+ const { client, project } = registry.clientFor(args.project);
2906
+ const result = await client.analytics.releaseImpact(project, args.release, {
2907
+ windowDays: args.windowDays
2908
+ });
2909
+ const moved = result.events.filter((e) => e.changePercent !== null && Math.abs(e.changePercent) >= 10).slice(0, 3).map((e) => `${e.name} ${e.changePercent}%`).join(", ");
2910
+ const summary = [
2911
+ `Release #${result.release.number} over ${result.windowDays} day(s):`,
2912
+ `visitors ${result.change.visitors ?? "\u2014"}%`,
2913
+ moved ? `notable: ${moved}` : null,
2914
+ `${result.feedback.after} report(s) since (${result.feedback.before} before)`
2915
+ ].filter(Boolean).join(" ");
2916
+ return ok(summary, result);
2917
+ } catch (error) {
2918
+ return fail(error);
2919
+ }
2920
+ }
2921
+ );
2922
+ server.registerTool(
2923
+ "myna_analytics_person",
2924
+ {
2925
+ title: "One person: what they did and what they said",
2926
+ description: "A single identified user's recent sessions, recent events, and the bug reports they filed, in one response. Only people whose identity the customer's own backend signed appear here \u2014 anonymous visitors have sessions but no profile, deliberately. Use it when a report needs reproducing: the events leading up to it are usually the reproduction steps the reporter did not write down.",
2927
+ inputSchema: {
2928
+ ...projectArg,
2929
+ person: z.string().describe("Myna profile id (apr_...) or the application's own user id.")
2930
+ },
2931
+ annotations: { readOnlyHint: true, openWorldHint: true }
2932
+ },
2933
+ async (args) => {
2934
+ try {
2935
+ const { client, project } = registry.clientFor(args.project);
2936
+ const person = await client.analytics.profile(project, args.person);
2937
+ return ok(
2938
+ `${person.displayName ?? person.externalId}: ${person.sessions} session(s), ${person.events} event(s), ${person.reports.length} report(s).`,
2939
+ person
2940
+ );
2941
+ } catch (error) {
2942
+ return fail(error);
2943
+ }
2944
+ }
2945
+ );
2946
+ }
2548
2947
 
2549
2948
  // src/resources.ts
2550
2949
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -2618,7 +3017,7 @@ function registerResources(server, registry) {
2618
3017
  }
2619
3018
 
2620
3019
  // src/version.ts
2621
- var VERSION = true ? "0.16.1" : "0.0.0-dev";
3020
+ var VERSION = true ? "0.17.0" : "0.0.0-dev";
2622
3021
 
2623
3022
  // src/server.ts
2624
3023
  var SERVER_NAME = "myna";