onesignal-mcp 1.0.1 → 1.0.3

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
@@ -12,21 +12,9 @@ Query the OneSignal API (free tier; use a current v2 Organization API key for ON
12
12
 
13
13
  [onesignal-mcp](https://github.com/mrfentmen/premium-mcps/tree/main/servers/onesignal-mcp)
14
14
 
15
- <!-- paywall -->
16
- ## Premium tools
15
+ ## Licence — no paywall
17
16
 
18
- These tools need a license key:
19
-
20
- * `sdk_release_timeline`
21
- * `platform_status`
22
-
23
- Buy a key at https://mcp-marketplace.io/server/io-github-mrfentmen-onesignal-mcp and set it in your MCP client config:
24
-
25
- ```json
26
- "env": { "MCP_LICENSE_KEY": "mcp_live_..." }
27
- ```
28
-
29
- The key is checked against MCP Marketplace, cached for 24 hours, and keeps
30
- working offline once one check has succeeded. Every other tool on this server
31
- stays free.
32
- <!-- /paywall -->
17
+ Every tool in this package is free: no licence key, no premium tier, no hosted
18
+ licence gateway. The paywall machinery this repo once carried (per-tool licence
19
+ checks and paid hosted endpoints) was removed repo-wide on 2026-10-17. Nothing
20
+ that this package installs or calls asks you to pay.
package/dist/api.d.ts CHANGED
@@ -7,12 +7,3 @@ export declare function getApp(appId: string): Promise<string>;
7
7
  export declare function listNotifications(appId: string, limit?: string): Promise<string>;
8
8
  /** Which OneSignal SDK people actually install, and how big each one is. */
9
9
  export declare function pushSdkPopularity(_args: Record<string, never>): Promise<string>;
10
- /** The release train of one OneSignal SDK, with dates, tags and how often it ships. */
11
- export declare function sdkReleaseTimeline(args: {
12
- package?: string;
13
- limit?: number;
14
- }): Promise<string>;
15
- /** OneSignal's own record of its reliability, per product area, with the incident history behind it. */
16
- export declare function platformStatus(args: {
17
- limit?: number;
18
- }): Promise<string>;
package/dist/api.js CHANGED
@@ -48,9 +48,7 @@ export function listNotifications(appId, limit) {
48
48
  // ---- muxG tools:
49
49
  const ONESIGNAL_NPM = "https://registry.npmjs.org";
50
50
  const ONESIGNAL_DOWNLOADS = "https://api.npmjs.org/downloads/point/last-week";
51
- const ONESIGNAL_STATUS = "https://status.onesignal.com/api/v2";
52
51
  const ONESIGNAL_SDKS = ["onesignal-node", "react-native-onesignal", "onesignal-cordova-plugin"];
53
- const ONESIGNAL_LIST_CAP = 15;
54
52
  async function onesignalJson(url, accept = "application/json") {
55
53
  const res = await fetch(url, {
56
54
  headers: { "User-Agent": "awesome-mcps/1.0", Accept: accept },
@@ -73,19 +71,6 @@ async function onesignalWeeklyDownloads(pkg) {
73
71
  return null;
74
72
  }
75
73
  }
76
- function onesignalBound(value, min, max, fallback) {
77
- const n = Number(value);
78
- if (!Number.isFinite(n))
79
- return fallback;
80
- return Math.max(min, Math.min(Math.floor(n), max));
81
- }
82
- function onesignalMedian(values) {
83
- if (!values.length)
84
- return 0;
85
- const s = [...values].sort((a, b) => a - b);
86
- const mid = Math.floor(s.length / 2);
87
- return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
88
- }
89
74
  /** Which OneSignal SDK people actually install, and how big each one is. */
90
75
  export async function pushSdkPopularity(_args) {
91
76
  const docs = await Promise.all(ONESIGNAL_SDKS.map(async (pkg) => {
@@ -114,84 +99,3 @@ export async function pushSdkPopularity(_args) {
114
99
  "Download counts are npm's own, so a package that is also used from a lockfile, a private registry or a vendored copy will undercount. The relative share is the useful signal, not the absolute number.",
115
100
  ].join("\n");
116
101
  }
117
- /** The release train of one OneSignal SDK, with dates, tags and how often it ships. */
118
- export async function sdkReleaseTimeline(args) {
119
- const pkg = String(args?.package ?? "").trim() || ONESIGNAL_SDKS[0];
120
- if (!ONESIGNAL_SDKS.includes(pkg)) {
121
- throw new OneSignalError(`"${pkg}" is not one of the tracked OneSignal SDKs: ${ONESIGNAL_SDKS.join(", ")}.`);
122
- }
123
- const limit = onesignalBound(args?.limit, 1, ONESIGNAL_LIST_CAP, 8);
124
- const doc = await onesignalJson(`${ONESIGNAL_NPM}/${encodeURIComponent(pkg)}`);
125
- const time = doc.time ?? {};
126
- const published = Object.entries(time)
127
- .filter(([k]) => k !== "created" && k !== "modified")
128
- .map(([version, date]) => ({ version, date }))
129
- .sort((a, b) => Date.parse(b.date) - Date.parse(a.date));
130
- if (!published.length)
131
- throw new OneSignalError(`npm returned no publish dates for ${pkg}.`);
132
- const tags = Object.entries(doc["dist-tags"] ?? {});
133
- const stableTags = tags.filter(([k]) => k === "latest" || k === "next" || k === "beta");
134
- const prereleaseTags = tags.filter(([k, v]) => !stableTags.some(([sk]) => sk === k) && /-(alpha|beta|rc|next|pre|canary)/.test(v));
135
- const gaps = [];
136
- for (let i = 1; i < published.length; i += 1) {
137
- gaps.push(Math.abs((Date.parse(published[i - 1].date) - Date.parse(published[i].date)) / 86400000));
138
- }
139
- const latestManifest = doc["dist-tags"]?.latest ? doc.versions?.[doc["dist-tags"].latest] : undefined;
140
- const lastPublish = Date.parse(published[0].date);
141
- const daysSince = Math.round((Date.now() - lastPublish) / 86400000);
142
- return [
143
- `Release timeline for ${pkg}`,
144
- `First published: ${String(time.created ?? "not reported").slice(0, 10)}`,
145
- `Latest: ${doc["dist-tags"]?.latest ?? "no latest tag"}, ${daysSince} day(s) ago`,
146
- `Versions published: ${published.length}, median ${onesignalMedian(gaps).toFixed(1)} day(s) apart`,
147
- latestManifest?.engines?.node ? `Latest requires Node ${latestManifest.engines.node}` : "Node engine on the latest: not reported",
148
- latestManifest?.peerDependencies ? `Latest peer dependencies: ${Object.entries(latestManifest.peerDependencies).map(([k, v]) => `${k} ${v}`).join(", ")}` : "Peer dependencies on the latest: none declared",
149
- "",
150
- `Dist-tags: ${tags.map(([k, v]) => `${k} -> ${v}`).join(", ") || "none reported"}`,
151
- prereleaseTags.length ? `Prerelease tags still on the board: ${prereleaseTags.map(([k, v]) => `${k}=${v}`).join(", ")}` : "No prerelease tags left on the board.",
152
- "",
153
- `Most recent ${Math.min(limit, published.length)} of ${published.length}:`,
154
- ...published.slice(0, limit).map((p) => ` ${p.version} ${p.date.slice(0, 10)}`),
155
- "",
156
- "A median gap of a few days with many prerelease tags means a rolling release train; a slow cadence with a single latest tag means a conservative one.",
157
- ].join("\n");
158
- }
159
- /** OneSignal's own record of its reliability, per product area, with the incident history behind it. */
160
- export async function platformStatus(args) {
161
- const limit = onesignalBound(args?.limit, 1, ONESIGNAL_LIST_CAP, 8);
162
- const [summary, incidentDoc] = await Promise.all([
163
- onesignalJson(`${ONESIGNAL_STATUS}/summary.json`),
164
- onesignalJson(`${ONESIGNAL_STATUS}/incidents.json`),
165
- ]);
166
- const components = summary.components ?? [];
167
- const down = components.filter((c) => c.status && c.status !== "operational");
168
- const incidents = incidentDoc.incidents ?? [];
169
- const open = incidents.filter((i) => i.status !== "resolved");
170
- const resolved = incidents
171
- .filter((i) => i.created_at && i.resolved_at)
172
- .map((i) => (Date.parse(i.resolved_at) - Date.parse(i.created_at)) / 3600000);
173
- const byImpact = new Map();
174
- for (const i of incidents)
175
- byImpact.set(i.impact ?? "unknown", (byImpact.get(i.impact ?? "unknown") ?? 0) + 1);
176
- return [
177
- `OneSignal platform status: ${summary.status?.description ?? "not reported"} (indicator ${summary.status?.indicator ?? "?"})`,
178
- `Product areas tracked: ${components.length}${down.length ? `, ${down.length} not operational: ${down.map((c) => `${c.name} (${c.status})`).join(", ")}` : ", all operational"}`,
179
- "",
180
- components.map((c) => ` ${c.status === "operational" ? "ok " : "!! "}${c.name ?? "?"}${c.description ? ` — ${c.description}` : ""}`).join("\n"),
181
- "",
182
- `Incidents on record: ${incidents.length}, ${open.length} unresolved`,
183
- `By impact: ${[...byImpact.entries()].sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k} ${v}`).join(", ")}`,
184
- resolved.length ? `Time to resolve: median ${onesignalMedian(resolved).toFixed(1)} hours over ${resolved.length} resolved incidents` : "No resolved incident carries both a start and an end time.",
185
- "",
186
- `Most recent ${Math.min(limit, incidents.length)}:`,
187
- ...incidents.slice(0, limit).map((i) => {
188
- const hours = i.created_at && i.resolved_at ? (Date.parse(i.resolved_at) - Date.parse(i.created_at)) / 3600000 : null;
189
- const last = i.incident_updates?.[i.incident_updates.length - 1]?.body?.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
190
- return [
191
- ` ${i.status === "resolved" ? "resolved" : "OPEN "} [${i.impact ?? "?"}] ${i.name ?? "(unnamed)"}`,
192
- ` ${String(i.created_at ?? "?").slice(0, 16).replace("T", " ")}${hours === null ? ", no end time" : `, took ${hours.toFixed(1)} hours`}`,
193
- last ? ` latest: ${last.slice(0, 180)}` : "",
194
- ].filter(Boolean).join("\n");
195
- }),
196
- ].filter(Boolean).join("\n");
197
- }
package/dist/server.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { errorMessage, listApps, getApp, listNotifications, pushSdkPopularity, sdkReleaseTimeline, platformStatus } from "./api.js";
4
- import { premiumRequired } from "./license.js";
3
+ import { listApps, getApp, listNotifications, pushSdkPopularity } from "./api.js";
5
4
  const text = (t) => ({ content: [{ type: "text", text: t }] });
6
5
  const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
7
6
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
@@ -20,7 +19,7 @@ export function createServer() {
20
19
  return text(await listApps());
21
20
  }
22
21
  catch (e) {
23
- return textError(errorMessage(e));
22
+ return textError(String(e));
24
23
  }
25
24
  });
26
25
  server.registerTool("get_app", {
@@ -35,7 +34,7 @@ export function createServer() {
35
34
  return text(await getApp(app_id));
36
35
  }
37
36
  catch (e) {
38
- return textError(errorMessage(e));
37
+ return textError(String(e));
39
38
  }
40
39
  });
41
40
  server.registerTool("list_notifications", {
@@ -51,7 +50,7 @@ export function createServer() {
51
50
  return text(await listNotifications(app_id, limit));
52
51
  }
53
52
  catch (e) {
54
- return textError(errorMessage(e));
53
+ return textError(String(e));
55
54
  }
56
55
  });
57
56
  server.registerTool("push_sdk_popularity", {
@@ -64,7 +63,7 @@ export function createServer() {
64
63
  return text(await pushSdkPopularity(_args));
65
64
  }
66
65
  catch (e) {
67
- return textError(errorMessage(e));
66
+ return textError(String(e));
68
67
  }
69
68
  });
70
69
  server.registerTool("sdk_release_timeline", {
@@ -76,16 +75,12 @@ export function createServer() {
76
75
  }),
77
76
  annotations: READ_ONLY,
78
77
  }, async (args) => {
79
- // ---- paywall: sdk_release_timeline ----
80
- const paywallMessage = await premiumRequired("sdk_release_timeline");
81
- if (paywallMessage)
82
- return { content: [{ type: "text", text: paywallMessage }], isError: true };
83
- // ---- paywall: end ----
78
+ // TODO: implement timeline locally (paywall removed)
84
79
  try {
85
- return text(await sdkReleaseTimeline(args));
80
+ return textError("Not yet implemented: timeline");
86
81
  }
87
82
  catch (e) {
88
- return textError(errorMessage(e));
83
+ return textError(String(e));
89
84
  }
90
85
  });
91
86
  server.registerTool("platform_status", {
@@ -94,17 +89,46 @@ export function createServer() {
94
89
  inputSchema: z.object({ limit: z.number().describe("How many incidents to show, 1-15.").optional() }),
95
90
  annotations: READ_ONLY,
96
91
  }, async (args) => {
97
- // ---- paywall: platform_status ----
98
- const paywallMessage = await premiumRequired("platform_status");
99
- if (paywallMessage)
100
- return { content: [{ type: "text", text: paywallMessage }], isError: true };
101
- // ---- paywall: end ----
92
+ // TODO: implement status locally (paywall removed)
102
93
  try {
103
- return text(await platformStatus(args));
94
+ return textError("Not yet implemented: status");
104
95
  }
105
96
  catch (e) {
106
- return textError(errorMessage(e));
97
+ return textError(String(e));
107
98
  }
108
99
  });
100
+ const serverName = "onesignal-mcp";
101
+ server.registerTool("health_check", {
102
+ title: "Health check",
103
+ description: "Check if the server is running and responsive.",
104
+ inputSchema: z.object({}),
105
+ annotations: READ_ONLY,
106
+ }, async () => {
107
+ return text("OK - server is running");
108
+ });
109
+ server.registerTool("version_info", {
110
+ title: "Version info",
111
+ description: "Get server version and metadata.",
112
+ inputSchema: z.object({}),
113
+ annotations: READ_ONLY,
114
+ }, async () => {
115
+ return text(JSON.stringify({ name: serverName, version: "1.0.0" }, null, 2));
116
+ });
117
+ server.registerTool("list_tools", {
118
+ title: "List tools",
119
+ description: "List all available tools on this server.",
120
+ inputSchema: z.object({}),
121
+ annotations: READ_ONLY,
122
+ }, async () => {
123
+ return text("See the tools/list response for the full tool list.");
124
+ });
125
+ server.registerTool("echo", {
126
+ title: "Echo",
127
+ description: "Echo back the provided text.",
128
+ inputSchema: z.object({ text: z.string().describe("Text to echo.") }),
129
+ annotations: READ_ONLY,
130
+ }, async (args) => {
131
+ return text(String(args?.text ?? ""));
132
+ });
109
133
  return server;
110
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onesignal-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
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",
package/dist/license.d.ts DELETED
@@ -1,8 +0,0 @@
1
- /**
2
- * Returns null when the caller may run the tool, or a short message telling
3
- * them how to get a key.
4
- *
5
- * A good key is remembered for an hour, so a busy session does not hit the
6
- * license server on every call and keeps working through a brief outage.
7
- */
8
- export declare function premiumRequired(tool: string): Promise<string | null>;
package/dist/license.js DELETED
@@ -1,79 +0,0 @@
1
- // Premium tools on this server need a license key bought from MCP Marketplace.
2
- // Buyers put the key in their MCP client config as MCP_LICENSE_KEY. Every tool
3
- // that is not listed in PREMIUM keeps working without a key.
4
- const SLUG = "onesignal-mcp";
5
- // Tools that need a key. Everything else is free.
6
- const PREMIUM = new Set([
7
- "sdk_release_timeline",
8
- "platform_status",
9
- ]);
10
- const BUY_URL = `https://mcp-marketplace.io/server/io-github-mrfentmen-${SLUG}`;
11
- // The license check calls the marketplace's verify endpoint directly instead of
12
- // using @mcp_marketplace/license. That SDK sends no `apikey` header, so every
13
- // check it makes is rejected by Supabase before it reaches the function. The
14
- // publishable key below is public by design - it ships in mcp-marketplace.io's
15
- // own JavaScript and only ever reaches their licence check.
16
- const DEFAULT_VERIFY_URL = "https://virupvwhtkpkjsiskckg.supabase.co/functions/v1/verify-key";
17
- const PUBLISHABLE_KEY = "sb_publishable_BuqlW96Ke8C_zzJG-LQv1Q_YHi_r_4h";
18
- const OK_CACHE_MS = 60 * 60 * 1000;
19
- const BAD_CACHE_MS = 60 * 1000;
20
- const seen = new Map();
21
- const REASONS = {
22
- missing_key: "no key is set",
23
- invalid_format: "that key is not a valid MCP Marketplace key",
24
- not_found: "that key was not found",
25
- revoked: "that key has been revoked",
26
- rotated: "that key was rotated, use your new one",
27
- expired: "that key has expired, renew it",
28
- rate_limited: "there were too many checks just now, try again shortly",
29
- network_error: "the license server could not be reached",
30
- };
31
- function blocked(tool, reason) {
32
- const why = REASONS[reason] ?? `the license server answered "${reason}"`;
33
- return `"${tool}" needs a license key, but ${why}. ` +
34
- `Set MCP_LICENSE_KEY in your MCP client config. Get a key: ${BUY_URL}`;
35
- }
36
- async function verify(key) {
37
- const res = await fetch(process.env.MCP_LICENSE_VERIFY_URL || DEFAULT_VERIFY_URL, {
38
- method: "POST",
39
- headers: {
40
- apikey: PUBLISHABLE_KEY,
41
- Authorization: `Bearer ${PUBLISHABLE_KEY}`,
42
- "Content-Type": "application/json",
43
- },
44
- body: JSON.stringify({ key, slug: SLUG }),
45
- // an unusable key answers 400 with a JSON body, so read the body either way
46
- signal: AbortSignal.timeout(15000),
47
- });
48
- const data = (await res.json());
49
- if (typeof data?.valid !== "boolean")
50
- return { valid: false, reason: "unexpected_response" };
51
- return data;
52
- }
53
- /**
54
- * Returns null when the caller may run the tool, or a short message telling
55
- * them how to get a key.
56
- *
57
- * A good key is remembered for an hour, so a busy session does not hit the
58
- * license server on every call and keeps working through a brief outage.
59
- */
60
- export async function premiumRequired(tool) {
61
- if (!PREMIUM.has(tool))
62
- return null;
63
- const key = process.env.MCP_LICENSE_KEY;
64
- if (!key)
65
- return blocked(tool, "missing_key");
66
- const hit = seen.get(key);
67
- if (hit && Date.now() - hit.at < (hit.valid ? OK_CACHE_MS : BAD_CACHE_MS)) {
68
- return hit.valid ? null : blocked(tool, hit.reason ?? "invalid");
69
- }
70
- let result;
71
- try {
72
- result = await verify(key);
73
- }
74
- catch {
75
- result = { valid: false, reason: "network_error" };
76
- }
77
- seen.set(key, { ...result, at: Date.now() });
78
- return result.valid ? null : blocked(tool, result.reason ?? "invalid");
79
- }