onesignal-mcp 1.0.3 → 1.1.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/dist/api.js CHANGED
@@ -71,6 +71,139 @@ async function onesignalWeeklyDownloads(pkg) {
71
71
  return null;
72
72
  }
73
73
  }
74
+ /** OneSignal's public status page, a Statuspage instance. */
75
+ const ONESIGNAL_STATUS = "https://status.onesignal.com/api/v2";
76
+ async function onesignalStatusJson(path) {
77
+ const url = `${ONESIGNAL_STATUS}${path}`;
78
+ const res = await fetch(url, {
79
+ headers: { "User-Agent": "awesome-mcps/1.0", Accept: "application/json" },
80
+ signal: AbortSignal.timeout(30000),
81
+ });
82
+ if (!res.ok)
83
+ throw new OneSignalError(`The status page returned ${res.status} for ${path.replace(/^\//, "")}.`);
84
+ return (await res.json());
85
+ }
86
+ function osDays(fromIso, toMs = Date.now()) {
87
+ const t = Date.parse(fromIso);
88
+ if (!Number.isFinite(t))
89
+ return null;
90
+ return Math.round((toMs - t) / 86400000);
91
+ }
92
+ function osDay(iso) {
93
+ const t = Date.parse(iso ?? "");
94
+ return Number.isFinite(t) ? new Date(t).toISOString().slice(0, 10) : "unknown";
95
+ }
96
+ function osSpan(ms) {
97
+ if (!Number.isFinite(ms))
98
+ return "unresolved";
99
+ if (ms < 0)
100
+ return "0 min (the page's own timestamps are out of order)";
101
+ const mins = Math.round(ms / 60000);
102
+ if (mins < 60)
103
+ return `${mins} min`;
104
+ const hours = Math.floor(mins / 60);
105
+ if (hours < 48)
106
+ return `${hours} h ${mins % 60} min`;
107
+ return `${Math.round(hours / 24)} day(s)`;
108
+ }
109
+ /** Release cadence of one OneSignal SDK, read from the npm packument's own time map. */
110
+ export async function sdkReleaseTimeline(args) {
111
+ const requested = String(args.package ?? "").trim();
112
+ const pkg = requested || "react-native-onesignal";
113
+ const limit = Math.min(Math.max(Number(args.limit ?? 10) || 10, 1), 30);
114
+ const doc = await onesignalJson(`${ONESIGNAL_NPM}/${encodeURIComponent(pkg)}`);
115
+ const time = doc.time ?? {};
116
+ const versions = Object.keys(doc.versions ?? {});
117
+ if (!versions.length)
118
+ throw new OneSignalError(`${pkg} has no published versions on npm.`);
119
+ const dated = versions
120
+ .map((v) => ({ version: v, at: time[v] }))
121
+ .filter((r) => typeof r.at === "string")
122
+ .sort((a, b) => Date.parse(a.at) - Date.parse(b.at));
123
+ if (!dated.length)
124
+ throw new OneSignalError(`npm published no dates for ${pkg}, so the cadence cannot be read.`);
125
+ const first = dated[0];
126
+ const latestTag = doc["dist-tags"]?.latest ?? null;
127
+ const latestDated = dated[dated.length - 1];
128
+ const gaps = [];
129
+ for (let i = 1; i < dated.length; i++)
130
+ gaps.push(Date.parse(dated[i].at) - Date.parse(dated[i - 1].at));
131
+ const sortedGaps = [...gaps].sort((a, b) => a - b);
132
+ const medianGap = sortedGaps.length
133
+ ? sortedGaps.length % 2
134
+ ? sortedGaps[(sortedGaps.length - 1) / 2]
135
+ : (sortedGaps[sortedGaps.length / 2 - 1] + sortedGaps[sortedGaps.length / 2]) / 2
136
+ : null;
137
+ const tags = Object.entries(doc["dist-tags"] ?? {}).map(([tag, version]) => ({
138
+ tag,
139
+ version,
140
+ at: time[version],
141
+ prerelease: /-(alpha|beta|rc|next|pre|dev|canary|staging)/i.test(String(version)),
142
+ }));
143
+ const prereleaseTags = tags.filter((t) => t.prerelease);
144
+ const recent = [...dated].reverse().slice(0, limit);
145
+ const rows = [
146
+ `${pkg} release train, from the npm registry`,
147
+ ` versions published ${versions.length}`,
148
+ ` first release ${osDay(first.at)} (${first.version})`,
149
+ ` latest tag ${latestTag ?? "not set"}${latestTag && time[latestTag] ? `, published ${osDay(time[latestTag])}` : ""}`,
150
+ ` newest version by date ${latestDated.version}, ${osDay(latestDated.at)}`,
151
+ ` days since a release ${osDays(latestDated.at) ?? "unknown"}`,
152
+ medianGap !== null ? ` median gap ${(medianGap / 86400000).toFixed(1)} day(s)` : "",
153
+ "",
154
+ `dist-tags (${tags.length}):`,
155
+ ...tags.map((t) => ` ${t.tag.padEnd(14)} ${t.version}${t.at ? ` — ${osDay(t.at)}` : ""}${t.prerelease ? " (prerelease)" : ""}`),
156
+ prereleaseTags.length
157
+ ? ` ${prereleaseTags.length} tag(s) still point at a prerelease: ${prereleaseTags.map((t) => t.tag).join(", ")}`
158
+ : " No tag points at a prerelease.",
159
+ "",
160
+ `Most recent ${recent.length} version(s) by publish date:`,
161
+ ...recent.map((r) => ` ${osDay(r.at)} ${r.version}`),
162
+ "",
163
+ "Dates are npm's own publish timestamps. A version republished under a new tag keeps its original date.",
164
+ ].filter(Boolean);
165
+ return rows.join("\n");
166
+ }
167
+ /** OneSignal's own reliability record: current state per product area plus incident history. */
168
+ export async function platformStatus(args) {
169
+ const limit = Math.min(Math.max(Number(args.limit ?? 10) || 10, 1), 25);
170
+ const [summary, history] = await Promise.all([
171
+ onesignalStatusJson("/summary.json"),
172
+ onesignalStatusJson("/incidents.json").catch(() => ({ incidents: [] })),
173
+ ]);
174
+ const state = summary.status ?? {};
175
+ const components = Array.isArray(summary.components) ? summary.components : [];
176
+ const incidents = Array.isArray(history.incidents) ? history.incidents : Array.isArray(summary.incidents) ? summary.incidents : [];
177
+ const ranked = [...incidents]
178
+ .sort((a, b) => Date.parse(b.created_at ?? 0) - Date.parse(a.created_at ?? 0))
179
+ .slice(0, limit);
180
+ const rows = [
181
+ `OneSignal platform status — ${String(state.description ?? "unknown")} (indicator: ${String(state.indicator ?? "none")})`,
182
+ "",
183
+ `Product areas (${components.length}):`,
184
+ ...components.map((c) => ` ${String(c.status ?? "unknown").padEnd(12)} ${c.name}${c.group ? ` — ${c.group}` : ""}`),
185
+ ];
186
+ if (!components.length)
187
+ rows.push(" The status page published no components.");
188
+ rows.push("", `Incident history (${incidents.length} on record, latest ${ranked.length} shown):`);
189
+ if (!ranked.length)
190
+ rows.push(" No incidents on record.");
191
+ for (const inc of ranked) {
192
+ const resolved = inc.resolved_at ?? (inc.status === "resolved" ? inc.updated_at : null);
193
+ const createdMs = Date.parse(inc.created_at ?? "");
194
+ const resolvedMs = resolved ? Date.parse(resolved) : NaN;
195
+ const took = Number.isFinite(createdMs) && Number.isFinite(resolvedMs) ? resolvedMs - createdMs : NaN;
196
+ const updates = Array.isArray(inc.incident_updates) ? inc.incident_updates : [];
197
+ const latest = updates[0] ?? null;
198
+ rows.push(` ${osDay(inc.created_at)} ${inc.name}`);
199
+ const duration = !resolved ? "still open" : Number.isFinite(took) ? `resolved in ${osSpan(took)}` : "resolved, duration not published";
200
+ rows.push(` impact ${String(inc.impact ?? "none")} | status ${String(inc.status ?? "unknown")} | ${duration}${inc.shortlink ? ` | ${inc.shortlink}` : ""}`);
201
+ if (latest)
202
+ rows.push(` latest update (${osDay(latest.created_at)}): ${String(latest.body ?? "").replace(/\s+/g, " ").slice(0, 240)}`);
203
+ }
204
+ rows.push("", "Read from status.onesignal.com, the vendor's own Statuspage. Incident text is theirs, not a measurement of this server.");
205
+ return rows.join("\n");
206
+ }
74
207
  /** Which OneSignal SDK people actually install, and how big each one is. */
75
208
  export async function pushSdkPopularity(_args) {
76
209
  const docs = await Promise.all(ONESIGNAL_SDKS.map(async (pkg) => {
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { listApps, getApp, listNotifications, pushSdkPopularity } from "./api.js";
3
+ import { listApps, getApp, listNotifications, pushSdkPopularity, sdkReleaseTimeline, platformStatus } from "./api.js";
4
4
  const text = (t) => ({ content: [{ type: "text", text: t }] });
5
5
  const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
6
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
@@ -75,9 +75,8 @@ export function createServer() {
75
75
  }),
76
76
  annotations: READ_ONLY,
77
77
  }, async (args) => {
78
- // TODO: implement timeline locally (paywall removed)
79
78
  try {
80
- return textError("Not yet implemented: timeline");
79
+ return text(await sdkReleaseTimeline(args));
81
80
  }
82
81
  catch (e) {
83
82
  return textError(String(e));
@@ -89,9 +88,8 @@ export function createServer() {
89
88
  inputSchema: z.object({ limit: z.number().describe("How many incidents to show, 1-15.").optional() }),
90
89
  annotations: READ_ONLY,
91
90
  }, async (args) => {
92
- // TODO: implement status locally (paywall removed)
93
91
  try {
94
- return textError("Not yet implemented: status");
92
+ return text(await platformStatus(args));
95
93
  }
96
94
  catch (e) {
97
95
  return textError(String(e));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onesignal-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Query the OneSignal API (free tier; use a current v2 Organization API key for ONESIGNAL_USER_KEY and the app REST API key for ONESIGNAL_APP_KEY, Basic auth).",
5
5
  "type": "module",
6
6
  "mcpName": "io.github.mrfentmen/onesignal-mcp",