@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 +1 -1
- package/dist/main.d.ts +2 -2
- package/dist/main.js +401 -2
- package/dist/main.js.map +1 -1
- package/package.json +2 -2
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
|
|
101
|
-
* never be shown
|
|
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.
|
|
3020
|
+
var VERSION = true ? "0.17.0" : "0.0.0-dev";
|
|
2622
3021
|
|
|
2623
3022
|
// src/server.ts
|
|
2624
3023
|
var SERVER_NAME = "myna";
|