onesignal-mcp 1.0.0 → 1.0.2
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 +21 -2
- package/dist/api.d.ts +2 -0
- package/dist/api.js +55 -1
- package/dist/gateway.d.ts +16 -0
- package/dist/gateway.js +85 -0
- package/dist/license.d.ts +8 -0
- package/dist/server.js +52 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,10 +4,29 @@ Query the OneSignal API (free tier; use a current v2 Organization API key for ON
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
-
- `list_apps` — OneSignal apps.
|
|
8
|
-
- `get_app` —
|
|
7
|
+
- `list_apps` — OneSignal apps the key can send to.
|
|
8
|
+
- `get_app` — One app by id, with platform and player count.
|
|
9
9
|
- `list_notifications` — Recent notifications for an app.
|
|
10
10
|
|
|
11
11
|
## Source
|
|
12
12
|
|
|
13
13
|
[onesignal-mcp](https://github.com/mrfentmen/premium-mcps/tree/main/servers/onesignal-mcp)
|
|
14
|
+
|
|
15
|
+
<!-- paywall -->
|
|
16
|
+
## Premium tools
|
|
17
|
+
|
|
18
|
+
These tools need a licence key:
|
|
19
|
+
|
|
20
|
+
* `sdk_release_timeline`
|
|
21
|
+
* `platform_status`
|
|
22
|
+
|
|
23
|
+
The tool bodies run on our gateway, not inside this package, so the key is what buys access. Buy one at https://buy.polar.sh/polar_cl_mrO7rS5LZxpqM7oAFFEA10i9121J240U40IIj4JRZO4 and set it in your MCP client config:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
"env": { "MCP_LICENSE_KEY": "polar_..." }
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The gateway checks the key against Polar and gives every key 1,000 free premium
|
|
30
|
+
calls a month before per-call billing starts. Every other tool on this server
|
|
31
|
+
stays free.
|
|
32
|
+
<!-- /paywall -->
|
package/dist/api.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export declare function errorMessage(e: unknown): string;
|
|
|
5
5
|
export declare function listApps(): Promise<string>;
|
|
6
6
|
export declare function getApp(appId: string): Promise<string>;
|
|
7
7
|
export declare function listNotifications(appId: string, limit?: string): Promise<string>;
|
|
8
|
+
/** Which OneSignal SDK people actually install, and how big each one is. */
|
|
9
|
+
export declare function pushSdkPopularity(_args: Record<string, never>): Promise<string>;
|
package/dist/api.js
CHANGED
|
@@ -24,7 +24,7 @@ function appHeaders() {
|
|
|
24
24
|
async function get(path, headers) {
|
|
25
25
|
let res;
|
|
26
26
|
try {
|
|
27
|
-
res = await fetch(BASE + path, { headers });
|
|
27
|
+
res = await fetch(BASE + path, { signal: AbortSignal.timeout(30000), headers });
|
|
28
28
|
}
|
|
29
29
|
catch (e) {
|
|
30
30
|
throw new OneSignalError(`Network error: ${e.message}`);
|
|
@@ -45,3 +45,57 @@ export function listNotifications(appId, limit) {
|
|
|
45
45
|
const n = Math.min(Number(limit ?? 10) || 10, 50);
|
|
46
46
|
return get(`/notifications?app_id=${encodeURIComponent(appId)}&limit=${n}`, appHeaders());
|
|
47
47
|
}
|
|
48
|
+
// ---- muxG tools:
|
|
49
|
+
const ONESIGNAL_NPM = "https://registry.npmjs.org";
|
|
50
|
+
const ONESIGNAL_DOWNLOADS = "https://api.npmjs.org/downloads/point/last-week";
|
|
51
|
+
const ONESIGNAL_SDKS = ["onesignal-node", "react-native-onesignal", "onesignal-cordova-plugin"];
|
|
52
|
+
async function onesignalJson(url, accept = "application/json") {
|
|
53
|
+
const res = await fetch(url, {
|
|
54
|
+
headers: { "User-Agent": "awesome-mcps/1.0", Accept: accept },
|
|
55
|
+
signal: AbortSignal.timeout(45000),
|
|
56
|
+
});
|
|
57
|
+
if (res.status === 404)
|
|
58
|
+
throw new OneSignalError(`Not found: ${url.replace(/^https:\/\//, "")}`);
|
|
59
|
+
if (res.status === 429)
|
|
60
|
+
throw new OneSignalError("The npm registry is rate limiting this IP. Wait a minute and try again.");
|
|
61
|
+
if (!res.ok)
|
|
62
|
+
throw new OneSignalError(`Returned ${res.status} for ${url.replace(/^https:\/\//, "")}`);
|
|
63
|
+
return (await res.json());
|
|
64
|
+
}
|
|
65
|
+
async function onesignalWeeklyDownloads(pkg) {
|
|
66
|
+
try {
|
|
67
|
+
const d = await onesignalJson(`${ONESIGNAL_DOWNLOADS}/${encodeURIComponent(pkg)}`);
|
|
68
|
+
return typeof d.downloads === "number" ? d.downloads : null;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Which OneSignal SDK people actually install, and how big each one is. */
|
|
75
|
+
export async function pushSdkPopularity(_args) {
|
|
76
|
+
const docs = await Promise.all(ONESIGNAL_SDKS.map(async (pkg) => {
|
|
77
|
+
const [doc, downloads] = await Promise.all([
|
|
78
|
+
onesignalJson(`${ONESIGNAL_NPM}/${encodeURIComponent(pkg)}`, "application/vnd.npm.install-v1+json"),
|
|
79
|
+
onesignalWeeklyDownloads(pkg),
|
|
80
|
+
]);
|
|
81
|
+
return { pkg, doc, downloads };
|
|
82
|
+
}));
|
|
83
|
+
const rows = docs.map(({ pkg, doc, downloads }) => {
|
|
84
|
+
const latest = doc["dist-tags"]?.latest;
|
|
85
|
+
const versions = Object.keys(doc.versions ?? {});
|
|
86
|
+
const stable = versions.filter((v) => !/-(alpha|beta|rc|next|pre|dev|canary)/.test(v));
|
|
87
|
+
return { pkg, latest, versions: versions.length, stable: stable.length, downloads, description: doc.description };
|
|
88
|
+
});
|
|
89
|
+
const total = rows.reduce((s, r) => s + (r.downloads ?? 0), 0);
|
|
90
|
+
return [
|
|
91
|
+
"OneSignal SDK adoption, from the npm registry and the public download counter",
|
|
92
|
+
...rows.map((r) => [
|
|
93
|
+
`${r.pkg} — ${r.latest ?? "no latest tag"}, ${r.versions} version(s) of which ${r.stable} are stable`,
|
|
94
|
+
r.downloads !== null ? ` weekly downloads: ${r.downloads.toLocaleString("en-US")}${total ? ` (${((r.downloads / total) * 100).toFixed(1)}% of these three)` : ""}` : " weekly downloads: not reported",
|
|
95
|
+
r.description ? ` ${r.description.slice(0, 130)}` : "",
|
|
96
|
+
].filter(Boolean).join("\n")),
|
|
97
|
+
"",
|
|
98
|
+
`Combined weekly downloads across the three SDKs: ${total.toLocaleString("en-US")}`,
|
|
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.",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the premium gateway.
|
|
3
|
+
*
|
|
4
|
+
* The premium tools do not run in this package any more. Their bodies live on
|
|
5
|
+
* the gateway, which checks a Polar licence key before it does any work, so
|
|
6
|
+
* deleting anything here buys nothing: the answer only exists behind the gate.
|
|
7
|
+
*
|
|
8
|
+
* Set `MCP_LICENSE_KEY` to a Polar licence key. `MCP_GATEWAY_URL` overrides the
|
|
9
|
+
* endpoint, which is only useful for testing.
|
|
10
|
+
*/
|
|
11
|
+
/** Raised when a premium call cannot be completed. `message` is safe to show. */
|
|
12
|
+
export declare class GatewayError extends Error {
|
|
13
|
+
readonly refused: boolean;
|
|
14
|
+
constructor(message: string, refused: boolean);
|
|
15
|
+
}
|
|
16
|
+
export declare function premiumTool(tool: string, args: Record<string, unknown>): Promise<string>;
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the premium gateway.
|
|
3
|
+
*
|
|
4
|
+
* The premium tools do not run in this package any more. Their bodies live on
|
|
5
|
+
* the gateway, which checks a Polar licence key before it does any work, so
|
|
6
|
+
* deleting anything here buys nothing: the answer only exists behind the gate.
|
|
7
|
+
*
|
|
8
|
+
* Set `MCP_LICENSE_KEY` to a Polar licence key. `MCP_GATEWAY_URL` overrides the
|
|
9
|
+
* endpoint, which is only useful for testing.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_GATEWAY_URL = 'https://mcp-gateway.contactae2000.workers.dev/mcp';
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 25000;
|
|
13
|
+
const USER_AGENT = 'mrfentmen-mcp/1.0';
|
|
14
|
+
/** Raised when a premium call cannot be completed. `message` is safe to show. */
|
|
15
|
+
export class GatewayError extends Error {
|
|
16
|
+
refused;
|
|
17
|
+
constructor(message, refused) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.refused = refused;
|
|
20
|
+
this.name = 'GatewayError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function premiumTool(tool, args) {
|
|
24
|
+
const key = (process.env.MCP_LICENSE_KEY ?? '').trim();
|
|
25
|
+
if (!key) {
|
|
26
|
+
throw new GatewayError(`"${tool}" is a premium tool and no licence key is set. Set MCP_LICENSE_KEY in your MCP client config.`, true);
|
|
27
|
+
}
|
|
28
|
+
const url = (process.env.MCP_GATEWAY_URL ?? DEFAULT_GATEWAY_URL).trim();
|
|
29
|
+
const controller = new AbortController();
|
|
30
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
31
|
+
let response;
|
|
32
|
+
try {
|
|
33
|
+
response = await fetch(url, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
'content-type': 'application/json',
|
|
37
|
+
accept: 'application/json',
|
|
38
|
+
'user-agent': USER_AGENT,
|
|
39
|
+
authorization: `Bearer ${key}`,
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
id: 1,
|
|
44
|
+
method: 'tools/call',
|
|
45
|
+
params: { name: tool, arguments: args },
|
|
46
|
+
}),
|
|
47
|
+
signal: controller.signal,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new GatewayError(`"${tool}" could not reach the licence gateway. Check your connection and try again.`, false);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new GatewayError(`"${tool}" could not reach the licence gateway (HTTP ${response.status}).`, false);
|
|
58
|
+
}
|
|
59
|
+
let body;
|
|
60
|
+
try {
|
|
61
|
+
body = (await response.json());
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new GatewayError(`"${tool}" got an unreadable answer from the licence gateway.`, false);
|
|
65
|
+
}
|
|
66
|
+
if (body.error) {
|
|
67
|
+
throw new GatewayError(`"${tool}" was refused by the licence gateway: ${body.error.message ?? 'unknown error'}`, false);
|
|
68
|
+
}
|
|
69
|
+
const result = body.result ?? {};
|
|
70
|
+
const text = (result.content ?? [])
|
|
71
|
+
.map((part) => part.text ?? '')
|
|
72
|
+
.join('\n')
|
|
73
|
+
.trim();
|
|
74
|
+
if (result.isError) {
|
|
75
|
+
// The gateway marks a licence refusal structurally, so a missing or rejected
|
|
76
|
+
// key is told apart from a tool that failed after the gate let it through.
|
|
77
|
+
if (result.structuredContent?.licenceRefusal) {
|
|
78
|
+
throw new GatewayError(text || `"${tool}" needs a valid licence key.`, true);
|
|
79
|
+
}
|
|
80
|
+
throw new GatewayError(text || `"${tool}" failed on the licence gateway.`, false);
|
|
81
|
+
}
|
|
82
|
+
if (!text)
|
|
83
|
+
throw new GatewayError(`"${tool}" returned no data.`, false);
|
|
84
|
+
return text;
|
|
85
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
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/server.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { errorMessage, listApps, getApp, listNotifications, } from "./api.js";
|
|
3
|
+
import { errorMessage, listApps, getApp, listNotifications, pushSdkPopularity } from "./api.js";
|
|
4
|
+
import { premiumTool } from "./gateway.js";
|
|
4
5
|
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
6
|
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
7
|
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
-
const MUTATING = { readOnlyHint: false, openWorldHint: true };
|
|
8
8
|
export function createServer() {
|
|
9
9
|
const server = new McpServer({
|
|
10
10
|
name: "onesignal-mcp",
|
|
@@ -12,7 +12,7 @@ export function createServer() {
|
|
|
12
12
|
});
|
|
13
13
|
server.registerTool("list_apps", {
|
|
14
14
|
title: "List apps",
|
|
15
|
-
description: "OneSignal apps.",
|
|
15
|
+
description: "OneSignal apps the key can send to.",
|
|
16
16
|
inputSchema: z.object({}),
|
|
17
17
|
annotations: READ_ONLY,
|
|
18
18
|
}, async ({}) => {
|
|
@@ -25,7 +25,7 @@ export function createServer() {
|
|
|
25
25
|
});
|
|
26
26
|
server.registerTool("get_app", {
|
|
27
27
|
title: "Get app",
|
|
28
|
-
description: "
|
|
28
|
+
description: "One app by id, with platform and player count.",
|
|
29
29
|
inputSchema: z.object({
|
|
30
30
|
app_id: z.string().describe("OneSignal app ID.")
|
|
31
31
|
}),
|
|
@@ -54,5 +54,53 @@ export function createServer() {
|
|
|
54
54
|
return textError(errorMessage(e));
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
|
+
server.registerTool("push_sdk_popularity", {
|
|
58
|
+
title: "Push SDK popularity",
|
|
59
|
+
description: "Which OneSignal SDK people actually install: for each of the Node, React Native and Cordova packages, the latest version, how many versions exist, and last week's npm download count with each one's share of the three.",
|
|
60
|
+
inputSchema: z.object({}),
|
|
61
|
+
annotations: READ_ONLY,
|
|
62
|
+
}, async (_args) => {
|
|
63
|
+
try {
|
|
64
|
+
return text(await pushSdkPopularity(_args));
|
|
65
|
+
}
|
|
66
|
+
catch (e) {
|
|
67
|
+
return textError(errorMessage(e));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
server.registerTool("sdk_release_timeline", {
|
|
71
|
+
title: "SDK release timeline",
|
|
72
|
+
description: "The release train of one OneSignal SDK: first and latest publish dates, days since the last release, how many versions exist, the median gap between them, every dist-tag with any prerelease tags still on the board, and the recent versions by date.",
|
|
73
|
+
inputSchema: z.object({
|
|
74
|
+
package: z.string().describe("onesignal-node, react-native-onesignal or onesignal-cordova-plugin.").optional(),
|
|
75
|
+
limit: z.number().describe("How many recent versions to list, 1-15.").optional()
|
|
76
|
+
}),
|
|
77
|
+
annotations: READ_ONLY,
|
|
78
|
+
}, async (args) => {
|
|
79
|
+
// ---- paywall: sdk_release_timeline ----
|
|
80
|
+
// The body runs on the gateway, behind the licence gate.
|
|
81
|
+
// ---- paywall: end ----
|
|
82
|
+
try {
|
|
83
|
+
return text(await premiumTool("mcp_onesignal_sdk_release_timeline", args));
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
return textError(errorMessage(e));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
server.registerTool("platform_status", {
|
|
90
|
+
title: "Platform status",
|
|
91
|
+
description: "OneSignal's own reliability record: the headline status, every product area (push, email, SMS, in-app messages, integrations, analytics) with its current state, and the incident history with impact, time to resolve and the latest update on each.",
|
|
92
|
+
inputSchema: z.object({ limit: z.number().describe("How many incidents to show, 1-15.").optional() }),
|
|
93
|
+
annotations: READ_ONLY,
|
|
94
|
+
}, async (args) => {
|
|
95
|
+
// ---- paywall: platform_status ----
|
|
96
|
+
// The body runs on the gateway, behind the licence gate.
|
|
97
|
+
// ---- paywall: end ----
|
|
98
|
+
try {
|
|
99
|
+
return text(await premiumTool("mcp_onesignal_platform_status", args));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
return textError(errorMessage(e));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
57
105
|
return server;
|
|
58
106
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "onesignal-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "OneSignal
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
7
7
|
"repository": {
|