@crashbytes/pusher-mcp-server 1.0.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/LICENSE +21 -0
- package/README.md +122 -0
- package/build/index.js +33 -0
- package/build/pusher-client.js +32 -0
- package/build/tools/authorize-channel.js +73 -0
- package/build/tools/get-channel-info.js +60 -0
- package/build/tools/get-presence-users.js +66 -0
- package/build/tools/list-channels.js +82 -0
- package/build/tools/terminate-user.js +35 -0
- package/build/tools/trigger-batch-events.js +56 -0
- package/build/tools/trigger-event.js +52 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CrashBytes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Pusher Channels MCP Server
|
|
2
|
+
|
|
3
|
+
An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that gives AI agents the ability to send realtime messages, query channels, and manage users through [Pusher Channels](https://pusher.com/channels).
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `trigger_event` | Send an event to one or more channels |
|
|
10
|
+
| `trigger_batch_events` | Send up to 10 events in a single API call |
|
|
11
|
+
| `list_channels` | List active channels with optional prefix filter |
|
|
12
|
+
| `get_channel_info` | Get subscription/user count for a channel |
|
|
13
|
+
| `get_presence_users` | List users connected to a presence channel |
|
|
14
|
+
| `authorize_channel` | Generate auth tokens for private/presence channels |
|
|
15
|
+
| `terminate_user_connections` | Disconnect all connections for a user |
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- Node.js 18 or later
|
|
20
|
+
- A [Pusher Channels](https://pusher.com) account (free tier available)
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @crashbytes/pusher-mcp-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or clone and build from source:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
git clone https://github.com/CrashBytes/pusher-mcp-server.git
|
|
32
|
+
cd pusher-mcp-server
|
|
33
|
+
npm install
|
|
34
|
+
npm run build
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
You need four environment variables from your [Pusher dashboard](https://dashboard.pusher.com):
|
|
40
|
+
|
|
41
|
+
| Variable | Description |
|
|
42
|
+
|----------|-------------|
|
|
43
|
+
| `PUSHER_APP_ID` | Your Pusher app ID |
|
|
44
|
+
| `PUSHER_KEY` | Your Pusher app key |
|
|
45
|
+
| `PUSHER_SECRET` | Your Pusher app secret |
|
|
46
|
+
| `PUSHER_CLUSTER` | Your Pusher cluster (e.g. `us2`, `eu`, `ap1`) |
|
|
47
|
+
|
|
48
|
+
## Usage with Claude Desktop
|
|
49
|
+
|
|
50
|
+
Add the following to your Claude Desktop config file:
|
|
51
|
+
|
|
52
|
+
**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
53
|
+
**Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"pusher": {
|
|
59
|
+
"command": "node",
|
|
60
|
+
"args": ["/path/to/pusher-mcp-server/build/index.js"],
|
|
61
|
+
"env": {
|
|
62
|
+
"PUSHER_APP_ID": "your_app_id",
|
|
63
|
+
"PUSHER_KEY": "your_app_key",
|
|
64
|
+
"PUSHER_SECRET": "your_app_secret",
|
|
65
|
+
"PUSHER_CLUSTER": "us2"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
If installed globally via npm:
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"mcpServers": {
|
|
77
|
+
"pusher": {
|
|
78
|
+
"command": "pusher-mcp-server",
|
|
79
|
+
"env": {
|
|
80
|
+
"PUSHER_APP_ID": "your_app_id",
|
|
81
|
+
"PUSHER_KEY": "your_app_key",
|
|
82
|
+
"PUSHER_SECRET": "your_app_secret",
|
|
83
|
+
"PUSHER_CLUSTER": "us2"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Usage with Claude Code
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
claude mcp add pusher -- node /path/to/pusher-mcp-server/build/index.js
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Set the environment variables in your shell before running, or configure them in your Claude Code MCP settings.
|
|
97
|
+
|
|
98
|
+
## Example Prompts
|
|
99
|
+
|
|
100
|
+
Once configured, you can ask Claude things like:
|
|
101
|
+
|
|
102
|
+
- "Send a 'deploy-complete' event to the notifications channel with the message 'v2.1.0 deployed'"
|
|
103
|
+
- "Show me all active presence channels"
|
|
104
|
+
- "How many users are on the presence-lobby channel?"
|
|
105
|
+
- "Disconnect user abc123 from all channels"
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install
|
|
111
|
+
npm run dev # Run with tsx (hot reload)
|
|
112
|
+
npm run build # Compile TypeScript
|
|
113
|
+
npm run type-check # Check types without emitting
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Tutorial
|
|
117
|
+
|
|
118
|
+
For a step-by-step guide on building this server from scratch, see the full tutorial on [CrashBytes](https://crashbytes.com/tutorials/building-pusher-channels-mcp-server-realtime-ai-2026).
|
|
119
|
+
|
|
120
|
+
## License
|
|
121
|
+
|
|
122
|
+
MIT
|
package/build/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/index.ts
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { registerTriggerEvent } from "./tools/trigger-event.js";
|
|
6
|
+
import { registerTriggerBatchEvents } from "./tools/trigger-batch-events.js";
|
|
7
|
+
import { registerListChannels } from "./tools/list-channels.js";
|
|
8
|
+
import { registerGetChannelInfo } from "./tools/get-channel-info.js";
|
|
9
|
+
import { registerGetPresenceUsers } from "./tools/get-presence-users.js";
|
|
10
|
+
import { registerAuthorizeChannel } from "./tools/authorize-channel.js";
|
|
11
|
+
import { registerTerminateUser } from "./tools/terminate-user.js";
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "pusher-channels",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
});
|
|
16
|
+
// Register all tools
|
|
17
|
+
registerTriggerEvent(server);
|
|
18
|
+
registerTriggerBatchEvents(server);
|
|
19
|
+
registerListChannels(server);
|
|
20
|
+
registerGetChannelInfo(server);
|
|
21
|
+
registerGetPresenceUsers(server);
|
|
22
|
+
registerAuthorizeChannel(server);
|
|
23
|
+
registerTerminateUser(server);
|
|
24
|
+
// Start the server
|
|
25
|
+
async function main() {
|
|
26
|
+
const transport = new StdioServerTransport();
|
|
27
|
+
await server.connect(transport);
|
|
28
|
+
console.error("Pusher MCP Server running on stdio");
|
|
29
|
+
}
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error("Fatal error:", error);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/pusher-client.ts
|
|
2
|
+
import Pusher from "pusher";
|
|
3
|
+
let client = null;
|
|
4
|
+
export function getPusherClient() {
|
|
5
|
+
if (client)
|
|
6
|
+
return client;
|
|
7
|
+
const appId = process.env.PUSHER_APP_ID;
|
|
8
|
+
const key = process.env.PUSHER_KEY;
|
|
9
|
+
const secret = process.env.PUSHER_SECRET;
|
|
10
|
+
const cluster = process.env.PUSHER_CLUSTER;
|
|
11
|
+
const missing = [];
|
|
12
|
+
if (!appId)
|
|
13
|
+
missing.push("PUSHER_APP_ID");
|
|
14
|
+
if (!key)
|
|
15
|
+
missing.push("PUSHER_KEY");
|
|
16
|
+
if (!secret)
|
|
17
|
+
missing.push("PUSHER_SECRET");
|
|
18
|
+
if (!cluster)
|
|
19
|
+
missing.push("PUSHER_CLUSTER");
|
|
20
|
+
if (missing.length > 0) {
|
|
21
|
+
throw new Error(`Missing required environment variables: ${missing.join(", ")}. ` +
|
|
22
|
+
"Set these in your MCP server configuration or .env file.");
|
|
23
|
+
}
|
|
24
|
+
client = new Pusher({
|
|
25
|
+
appId: appId,
|
|
26
|
+
key: key,
|
|
27
|
+
secret: secret,
|
|
28
|
+
cluster: cluster,
|
|
29
|
+
useTLS: true,
|
|
30
|
+
});
|
|
31
|
+
return client;
|
|
32
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerAuthorizeChannel(server) {
|
|
4
|
+
server.tool("authorize_channel", "Generate an authorization token for a private or presence channel. Useful when building auth endpoints for Pusher client connections.", {
|
|
5
|
+
socketId: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.describe("The socket ID from the client connection"),
|
|
9
|
+
channel: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(200)
|
|
13
|
+
.describe("Private or presence channel name (must start with 'private-' or 'presence-')"),
|
|
14
|
+
presenceData: z
|
|
15
|
+
.object({
|
|
16
|
+
user_id: z.string().min(1).describe("Unique user identifier"),
|
|
17
|
+
user_info: z
|
|
18
|
+
.record(z.unknown())
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("Optional user metadata (name, avatar, etc.)"),
|
|
21
|
+
})
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Required for presence channels — identifies the connecting user"),
|
|
24
|
+
}, async ({ socketId, channel, presenceData }) => {
|
|
25
|
+
try {
|
|
26
|
+
if (!channel.startsWith("private-") &&
|
|
27
|
+
!channel.startsWith("presence-")) {
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: 'Channel must start with "private-" or "presence-" for authorization',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
isError: true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
if (channel.startsWith("presence-") && !presenceData) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "presenceData is required for presence channels",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
isError: true,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const pusher = getPusherClient();
|
|
50
|
+
const auth = pusher.authorizeChannel(socketId, channel, presenceData);
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: `Authorization for ${channel}:\n${JSON.stringify(auth, null, 2)}`,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `Failed to authorize channel: ${message}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
isError: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerGetChannelInfo(server) {
|
|
4
|
+
server.tool("get_channel_info", "Get detailed information about a specific Pusher channel, including whether it is occupied and optional subscription/user counts.", {
|
|
5
|
+
channel: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.max(200)
|
|
9
|
+
.describe("The channel name to query"),
|
|
10
|
+
info: z
|
|
11
|
+
.array(z.enum(["user_count", "subscription_count"]))
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("Additional attributes to request"),
|
|
14
|
+
}, async ({ channel, info }) => {
|
|
15
|
+
try {
|
|
16
|
+
const pusher = getPusherClient();
|
|
17
|
+
const params = {};
|
|
18
|
+
if (info && info.length > 0)
|
|
19
|
+
params.info = info.join(",");
|
|
20
|
+
const response = await pusher.get({
|
|
21
|
+
path: `/channels/${encodeURIComponent(channel)}`,
|
|
22
|
+
params,
|
|
23
|
+
});
|
|
24
|
+
if (response.status !== 200) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: `Pusher API returned status ${response.status} for channel "${channel}"`,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
isError: true,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const body = (await response.json());
|
|
36
|
+
const lines = [`Channel: ${channel}`];
|
|
37
|
+
if (body.occupied !== undefined)
|
|
38
|
+
lines.push(`Occupied: ${body.occupied}`);
|
|
39
|
+
if (body.subscription_count !== undefined)
|
|
40
|
+
lines.push(`Subscriptions: ${body.subscription_count}`);
|
|
41
|
+
if (body.user_count !== undefined)
|
|
42
|
+
lines.push(`Users: ${body.user_count}`);
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `Failed to get channel info: ${message}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerGetPresenceUsers(server) {
|
|
4
|
+
server.tool("get_presence_users", "List all users currently connected to a presence channel. Only works with channels that start with 'presence-'.", {
|
|
5
|
+
channel: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.max(200)
|
|
9
|
+
.startsWith("presence-", {
|
|
10
|
+
message: "Channel must be a presence channel (starts with 'presence-')",
|
|
11
|
+
})
|
|
12
|
+
.describe("Presence channel name (must start with 'presence-')"),
|
|
13
|
+
}, async ({ channel }) => {
|
|
14
|
+
try {
|
|
15
|
+
const pusher = getPusherClient();
|
|
16
|
+
const response = await pusher.get({
|
|
17
|
+
path: `/channels/${encodeURIComponent(channel)}/users`,
|
|
18
|
+
params: {},
|
|
19
|
+
});
|
|
20
|
+
if (response.status !== 200) {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `Pusher API returned status ${response.status} for channel "${channel}"`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
isError: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const body = (await response.json());
|
|
32
|
+
const users = body.users || [];
|
|
33
|
+
if (users.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: `No users connected to ${channel}`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const userList = users.map((u) => ` ${u.id}`).join("\n");
|
|
44
|
+
return {
|
|
45
|
+
content: [
|
|
46
|
+
{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `Users on ${channel} (${users.length}):\n${userList}`,
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: "text",
|
|
59
|
+
text: `Failed to get presence users: ${message}`,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerListChannels(server) {
|
|
4
|
+
server.tool("list_channels", "List all active channels in your Pusher app. Optionally filter by prefix (e.g. 'presence-' or 'private-') and request subscription or user counts.", {
|
|
5
|
+
prefix: z
|
|
6
|
+
.string()
|
|
7
|
+
.optional()
|
|
8
|
+
.describe("Filter channels by prefix (e.g. 'presence-', 'private-chat-')"),
|
|
9
|
+
info: z
|
|
10
|
+
.array(z.enum(["user_count", "subscription_count"]))
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Additional attributes to include for each channel"),
|
|
13
|
+
}, async ({ prefix, info }) => {
|
|
14
|
+
try {
|
|
15
|
+
const pusher = getPusherClient();
|
|
16
|
+
const params = {};
|
|
17
|
+
if (prefix)
|
|
18
|
+
params.filter_by_prefix = prefix;
|
|
19
|
+
if (info && info.length > 0)
|
|
20
|
+
params.info = info.join(",");
|
|
21
|
+
const response = await pusher.get({
|
|
22
|
+
path: "/channels",
|
|
23
|
+
params,
|
|
24
|
+
});
|
|
25
|
+
if (response.status !== 200) {
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `Pusher API returned status ${response.status}`,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
isError: true,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const body = (await response.json());
|
|
37
|
+
const channels = body.channels || {};
|
|
38
|
+
const names = Object.keys(channels);
|
|
39
|
+
if (names.length === 0) {
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: prefix
|
|
45
|
+
? `No active channels matching prefix "${prefix}"`
|
|
46
|
+
: "No active channels",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const lines = names.map((name) => {
|
|
52
|
+
const ch = channels[name];
|
|
53
|
+
const parts = [name];
|
|
54
|
+
if (ch.subscription_count !== undefined)
|
|
55
|
+
parts.push(`subscriptions: ${ch.subscription_count}`);
|
|
56
|
+
if (ch.user_count !== undefined)
|
|
57
|
+
parts.push(`users: ${ch.user_count}`);
|
|
58
|
+
return parts.join(" — ");
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `Active channels (${names.length}):\n${lines.join("\n")}`,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: "text",
|
|
75
|
+
text: `Failed to list channels: ${message}`,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerTerminateUser(server) {
|
|
4
|
+
server.tool("terminate_user_connections", "Disconnect all connections for a specific user. Useful for moderation or security — forces a user offline across all channels.", {
|
|
5
|
+
userId: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.describe("The user ID to disconnect from all channels"),
|
|
9
|
+
}, async ({ userId }) => {
|
|
10
|
+
try {
|
|
11
|
+
const pusher = getPusherClient();
|
|
12
|
+
await pusher.terminateUserConnections(userId);
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: `All connections terminated for user "${userId}"`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: `Failed to terminate user connections: ${message}`,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
isError: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerTriggerBatchEvents(server) {
|
|
4
|
+
server.tool("trigger_batch_events", "Send up to 10 events in a single API call. More efficient than triggering events individually when you need to notify multiple channels.", {
|
|
5
|
+
events: z
|
|
6
|
+
.array(z.object({
|
|
7
|
+
channel: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(200)
|
|
11
|
+
.describe("Target channel name"),
|
|
12
|
+
name: z.string().min(1).max(200).describe("Event name"),
|
|
13
|
+
data: z
|
|
14
|
+
.union([z.string(), z.record(z.unknown())])
|
|
15
|
+
.describe("Event payload"),
|
|
16
|
+
socketId: z.string().optional().describe("Socket ID to exclude"),
|
|
17
|
+
}))
|
|
18
|
+
.min(1)
|
|
19
|
+
.max(10)
|
|
20
|
+
.describe("Array of events to send (max 10)"),
|
|
21
|
+
}, async ({ events }) => {
|
|
22
|
+
try {
|
|
23
|
+
const pusher = getPusherClient();
|
|
24
|
+
const batch = events.map((e) => ({
|
|
25
|
+
channel: e.channel,
|
|
26
|
+
name: e.name,
|
|
27
|
+
data: typeof e.data === "string" ? e.data : JSON.stringify(e.data),
|
|
28
|
+
...(e.socketId ? { socket_id: e.socketId } : {}),
|
|
29
|
+
}));
|
|
30
|
+
await pusher.triggerBatch(batch);
|
|
31
|
+
const summary = events
|
|
32
|
+
.map((e) => ` "${e.name}" → ${e.channel}`)
|
|
33
|
+
.join("\n");
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: `Batch of ${events.length} event(s) triggered:\n${summary}`,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: "text",
|
|
49
|
+
text: `Failed to trigger batch events: ${message}`,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
isError: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPusherClient } from "../pusher-client.js";
|
|
3
|
+
export function registerTriggerEvent(server) {
|
|
4
|
+
server.tool("trigger_event", "Send an event to one or more Pusher channels. Use this to push realtime messages to connected clients.", {
|
|
5
|
+
channel: z
|
|
6
|
+
.union([
|
|
7
|
+
z.string().min(1).max(200),
|
|
8
|
+
z.array(z.string().min(1).max(200)).min(1).max(100),
|
|
9
|
+
])
|
|
10
|
+
.describe("Channel name or array of channel names (max 100)"),
|
|
11
|
+
event: z
|
|
12
|
+
.string()
|
|
13
|
+
.min(1)
|
|
14
|
+
.max(200)
|
|
15
|
+
.describe("Event name to trigger (e.g. 'new-message', 'update')"),
|
|
16
|
+
data: z
|
|
17
|
+
.union([z.string(), z.record(z.unknown())])
|
|
18
|
+
.describe("Event payload — string or JSON object (max 10KB)"),
|
|
19
|
+
socketId: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Optional socket ID to exclude from receiving the event (prevents echo)"),
|
|
23
|
+
}, async ({ channel, event, data, socketId }) => {
|
|
24
|
+
try {
|
|
25
|
+
const pusher = getPusherClient();
|
|
26
|
+
const payload = typeof data === "string" ? data : JSON.stringify(data);
|
|
27
|
+
const params = socketId ? { socket_id: socketId } : undefined;
|
|
28
|
+
await pusher.trigger(channel, event, payload, params);
|
|
29
|
+
const channels = Array.isArray(channel) ? channel : [channel];
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
text: `Event "${event}" triggered on ${channels.length} channel(s): ${channels.join(", ")}`,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
41
|
+
return {
|
|
42
|
+
content: [
|
|
43
|
+
{
|
|
44
|
+
type: "text",
|
|
45
|
+
text: `Failed to trigger event: ${message}`,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
isError: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crashbytes/pusher-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Pusher Channels — trigger events, query channels, and manage realtime messaging from AI agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "build/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pusher-mcp-server": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc && chmod 755 build/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"type-check": "tsc --noEmit",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"build"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"mcp",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"pusher",
|
|
24
|
+
"realtime",
|
|
25
|
+
"websockets",
|
|
26
|
+
"ai",
|
|
27
|
+
"claude"
|
|
28
|
+
],
|
|
29
|
+
"mcpName": "io.github.crashbytes/pusher-mcp-server",
|
|
30
|
+
"author": "CrashBytes",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/CrashBytes/pusher-mcp-server.git"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
38
|
+
"pusher": "^5.2.0",
|
|
39
|
+
"zod": "^3.24.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^22.0.0",
|
|
43
|
+
"tsx": "^4.0.0",
|
|
44
|
+
"typescript": "^5.7.0",
|
|
45
|
+
"vitest": "^4.0.18"
|
|
46
|
+
}
|
|
47
|
+
}
|