@zeph-to/mcp-server 0.2.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 +242 -0
- package/dist/api-client.d.ts +54 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +106 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +17 -0
- package/dist/error-format.d.ts +8 -0
- package/dist/error-format.d.ts.map +1 -0
- package/dist/error-format.js +49 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +66 -0
- package/dist/poll.d.ts +10 -0
- package/dist/poll.d.ts.map +1 -0
- package/dist/poll.js +43 -0
- package/dist/resources/channels.d.ts +4 -0
- package/dist/resources/channels.d.ts.map +1 -0
- package/dist/resources/channels.js +24 -0
- package/dist/resources/devices.d.ts +4 -0
- package/dist/resources/devices.d.ts.map +1 -0
- package/dist/resources/devices.js +24 -0
- package/dist/tools/broadcast.d.ts +4 -0
- package/dist/tools/broadcast.d.ts.map +1 -0
- package/dist/tools/broadcast.js +36 -0
- package/dist/tools/clipboard.d.ts +5 -0
- package/dist/tools/clipboard.d.ts.map +1 -0
- package/dist/tools/clipboard.js +28 -0
- package/dist/tools/dismiss.d.ts +5 -0
- package/dist/tools/dismiss.d.ts.map +1 -0
- package/dist/tools/dismiss.js +37 -0
- package/dist/tools/file.d.ts +5 -0
- package/dist/tools/file.d.ts.map +1 -0
- package/dist/tools/file.js +59 -0
- package/dist/tools/input.d.ts +5 -0
- package/dist/tools/input.d.ts.map +1 -0
- package/dist/tools/input.js +46 -0
- package/dist/tools/list.d.ts +4 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/list.js +38 -0
- package/dist/tools/notify.d.ts +5 -0
- package/dist/tools/notify.d.ts.map +1 -0
- package/dist/tools/notify.js +36 -0
- package/dist/tools/prompt.d.ts +5 -0
- package/dist/tools/prompt.d.ts.map +1 -0
- package/dist/tools/prompt.js +59 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# @zeph-to/mcp-server
|
|
2
|
+
|
|
3
|
+
Zeph MCP server for AI agents. Send notifications, copy to clipboard, request confirmations, and collect text input from users across their devices — all via the [Model Context Protocol](https://modelcontextprotocol.io).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
### Claude Code
|
|
8
|
+
|
|
9
|
+
Add to `~/.claude/settings.json`:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"mcpServers": {
|
|
14
|
+
"zeph": {
|
|
15
|
+
"command": "npx",
|
|
16
|
+
"args": ["-y", "@zeph-to/mcp-server"],
|
|
17
|
+
"env": {
|
|
18
|
+
"ZEPH_API_KEY": "ak_...",
|
|
19
|
+
"ZEPH_HOOK_ID": "hook_...",
|
|
20
|
+
"ZEPH_DEVICE_ID": "dev_..."
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Cursor / Other MCP Clients
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "@zeph-to/mcp-server"],
|
|
33
|
+
"env": {
|
|
34
|
+
"ZEPH_API_KEY": "ak_..."
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Environment Variables
|
|
40
|
+
|
|
41
|
+
| Variable | Required | Description |
|
|
42
|
+
|----------|----------|-------------|
|
|
43
|
+
| `ZEPH_API_KEY` | Yes | API key from Settings > API Keys |
|
|
44
|
+
| `ZEPH_HOOK_ID` | No | Hook ID for interactive tools (`zeph_prompt`, `zeph_input`) |
|
|
45
|
+
| `ZEPH_DEVICE_ID` | No | Target device ID. Omit to send to all devices |
|
|
46
|
+
| `ZEPH_BASE_URL` | No | API base URL (default: `https://api.zeph.to/v1`). Use `https://api.zeph.to/d1` for dev |
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
### zeph_notify
|
|
51
|
+
|
|
52
|
+
Send a one-way push notification. Supports optional URL (auto-switches to link type).
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
title: "Build complete"
|
|
56
|
+
body: "All 42 tests passed"
|
|
57
|
+
url: "https://github.com/org/repo/actions/runs/123" (optional)
|
|
58
|
+
priority: "low" | "normal" | "high" | "urgent"
|
|
59
|
+
targetDeviceId: "dev_..." (optional, overrides ZEPH_DEVICE_ID)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### zeph_clipboard
|
|
63
|
+
|
|
64
|
+
Copy text to the user's device clipboard.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
text: "npm install @zeph-to/mcp-server"
|
|
68
|
+
targetDeviceId: "dev_..." (optional)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### zeph_list
|
|
72
|
+
|
|
73
|
+
List recent push notifications.
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
limit: 5 (1-20, default: 5)
|
|
77
|
+
type: "note" (optional filter: note, link, file, clipboard, hook)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Returns: `{ pushes: [...], total: 5, hasMore: true }`
|
|
81
|
+
|
|
82
|
+
### zeph_dismiss
|
|
83
|
+
|
|
84
|
+
Mark a specific push as read.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
pushId: "push_01HX..."
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### zeph_dismiss_all
|
|
91
|
+
|
|
92
|
+
Clear all notifications at once. No parameters.
|
|
93
|
+
|
|
94
|
+
Returns: `{ dismissed: 12, badge: 0 }`
|
|
95
|
+
|
|
96
|
+
### zeph_broadcast
|
|
97
|
+
|
|
98
|
+
Send a notification to all subscribers of a channel.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
channelId: "ch_..."
|
|
102
|
+
title: "Deploy complete"
|
|
103
|
+
body: "v2.1.0 is live"
|
|
104
|
+
url: "https://..." (optional)
|
|
105
|
+
priority: "normal"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### zeph_file
|
|
109
|
+
|
|
110
|
+
Send a text file to the user's device.
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
fileName: "report.json"
|
|
114
|
+
content: "{\"status\": \"ok\"}"
|
|
115
|
+
title: "Build Report" (optional, defaults to fileName)
|
|
116
|
+
targetDeviceId: "dev_..." (optional)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Returns: `{ pushId: "...", fileKey: "...", fileSize: 42 }`
|
|
120
|
+
|
|
121
|
+
### zeph_prompt
|
|
122
|
+
|
|
123
|
+
Ask the user to choose from 2-4 options. Blocks until response or timeout.
|
|
124
|
+
|
|
125
|
+
Requires `ZEPH_HOOK_ID`.
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
title: "Deploy to production?"
|
|
129
|
+
body: "3 migrations pending"
|
|
130
|
+
actions: [{ id: "yes", label: "Deploy", style: "primary" },
|
|
131
|
+
{ id: "no", label: "Cancel", style: "danger" }]
|
|
132
|
+
timeout: 120 (seconds, default: 120, max: 300)
|
|
133
|
+
fallback: "no" (auto-select on timeout, optional)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Returns: `{ actionId: "yes", timedOut: false }`
|
|
137
|
+
|
|
138
|
+
### zeph_input
|
|
139
|
+
|
|
140
|
+
Request free-form text input from the user. Blocks until response or timeout.
|
|
141
|
+
|
|
142
|
+
Requires `ZEPH_HOOK_ID`.
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
title: "Commit message"
|
|
146
|
+
body: "Summarize the changes"
|
|
147
|
+
placeholder: "feat: ..."
|
|
148
|
+
inputType: "text" | "password" | "multiline"
|
|
149
|
+
timeout: 120 (seconds, default: 120, max: 600)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Returns: `{ value: "feat: add clipboard sync", timedOut: false }`
|
|
153
|
+
|
|
154
|
+
## Resources
|
|
155
|
+
|
|
156
|
+
### zeph://devices
|
|
157
|
+
|
|
158
|
+
Lists connected devices with online status. Use to check which devices will receive notifications.
|
|
159
|
+
|
|
160
|
+
### zeph://channels
|
|
161
|
+
|
|
162
|
+
Lists channels the user owns or subscribes to. Use to find `channelId` for `zeph_broadcast`.
|
|
163
|
+
|
|
164
|
+
## Usage Guide
|
|
165
|
+
|
|
166
|
+
### When to use each tool
|
|
167
|
+
|
|
168
|
+
| Situation | Tool | Example |
|
|
169
|
+
|-----------|------|---------|
|
|
170
|
+
| Long task finished | `zeph_notify` | Build complete, test results, deploy done |
|
|
171
|
+
| Need user decision | `zeph_prompt` | Choose deploy target, confirm destructive action |
|
|
172
|
+
| Need free-form input | `zeph_input` | Commit message, env var value, description |
|
|
173
|
+
| Share code/logs | `zeph_file` | Error logs, test reports, generated config |
|
|
174
|
+
| Share snippet | `zeph_clipboard` | API key, URL, shell command |
|
|
175
|
+
|
|
176
|
+
### Recommended patterns
|
|
177
|
+
|
|
178
|
+
**Task completion notification:**
|
|
179
|
+
```
|
|
180
|
+
zeph_notify(
|
|
181
|
+
title: "Build complete: web app",
|
|
182
|
+
body: "All 42 tests passed. Bundle size: 1.2MB (-3%)"
|
|
183
|
+
)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Decision gate in CI/deploy flow:**
|
|
187
|
+
```
|
|
188
|
+
zeph_prompt(
|
|
189
|
+
title: "Deploy to production?",
|
|
190
|
+
body: "3 migrations pending. Last deploy: 2h ago.",
|
|
191
|
+
actions: [
|
|
192
|
+
{ id: "deploy", label: "Deploy", style: "primary" },
|
|
193
|
+
{ id: "staging", label: "Staging only", style: "secondary" },
|
|
194
|
+
{ id: "cancel", label: "Cancel", style: "danger" }
|
|
195
|
+
],
|
|
196
|
+
fallback: "cancel"
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Collecting user input remotely:**
|
|
201
|
+
```
|
|
202
|
+
zeph_input(
|
|
203
|
+
title: "Commit message",
|
|
204
|
+
body: "Changed: hooks.ts, input.ts, prompt.ts",
|
|
205
|
+
placeholder: "feat: ..."
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Error alert with link:**
|
|
210
|
+
```
|
|
211
|
+
zeph_notify(
|
|
212
|
+
title: "CI failed: lint errors",
|
|
213
|
+
body: "2 errors in src/auth.ts",
|
|
214
|
+
url: "https://github.com/org/repo/actions/runs/456",
|
|
215
|
+
priority: "high"
|
|
216
|
+
)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### When NOT to use
|
|
220
|
+
|
|
221
|
+
- Short responses the user can see immediately in the terminal
|
|
222
|
+
- Read-only operations (file search, code analysis)
|
|
223
|
+
- Every single tool call — only notify on meaningful milestones
|
|
224
|
+
|
|
225
|
+
### Multi-session workflow
|
|
226
|
+
|
|
227
|
+
When running multiple AI agent sessions in parallel, use `zeph_notify` to signal completion so the user knows which session finished without checking each terminal.
|
|
228
|
+
|
|
229
|
+
## API Key Permissions
|
|
230
|
+
|
|
231
|
+
The API key needs the following scopes:
|
|
232
|
+
|
|
233
|
+
- `push:read` — for `zeph_list`
|
|
234
|
+
- `push:write` — for `zeph_notify`, `zeph_clipboard`, `zeph_dismiss`, `zeph_dismiss_all`, `zeph_file`
|
|
235
|
+
- `hook:write` — for `zeph_prompt` and `zeph_input`
|
|
236
|
+
- `channel:read` — for `zeph://channels` resource
|
|
237
|
+
|
|
238
|
+
Create an API key with the **MCP** preset in Settings > API Keys for the correct permissions.
|
|
239
|
+
|
|
240
|
+
## License
|
|
241
|
+
|
|
242
|
+
MIT
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { McpServerConfig } from './config.js';
|
|
2
|
+
import type { PushResponse, HookTriggerResponse, HookEventResponse, DevicesResponse, PushListResponse, DismissResponse, ChannelsResponse, UploadRequestResponse } from './types.js';
|
|
3
|
+
export declare class ApiError extends Error {
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly status: number;
|
|
6
|
+
constructor(message: string, code: string, status: number);
|
|
7
|
+
}
|
|
8
|
+
export declare class ZephApiClient {
|
|
9
|
+
private readonly apiKey;
|
|
10
|
+
private readonly baseUrl;
|
|
11
|
+
constructor(config: McpServerConfig);
|
|
12
|
+
sendPush(params: {
|
|
13
|
+
title: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
url?: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
priority?: string;
|
|
18
|
+
targetDeviceId?: string;
|
|
19
|
+
channelId?: string;
|
|
20
|
+
fileKey?: string;
|
|
21
|
+
fileName?: string;
|
|
22
|
+
fileSize?: number;
|
|
23
|
+
fileType?: string;
|
|
24
|
+
}): Promise<PushResponse>;
|
|
25
|
+
triggerHook(hookId: string, params: {
|
|
26
|
+
title: string;
|
|
27
|
+
body?: string;
|
|
28
|
+
actions?: {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
}[];
|
|
32
|
+
timeout?: number;
|
|
33
|
+
fallback?: string;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
hookType?: 'one-way' | 'interactive' | 'input';
|
|
36
|
+
}): Promise<HookTriggerResponse>;
|
|
37
|
+
getHookEvent(hookId: string, eventId: string): Promise<HookEventResponse>;
|
|
38
|
+
listDevices(): Promise<DevicesResponse>;
|
|
39
|
+
listPushes(params?: {
|
|
40
|
+
limit?: number;
|
|
41
|
+
type?: string;
|
|
42
|
+
}): Promise<PushListResponse>;
|
|
43
|
+
dismissPush(pushId: string): Promise<DismissResponse>;
|
|
44
|
+
dismissAllPushes(): Promise<DismissResponse>;
|
|
45
|
+
listChannels(): Promise<ChannelsResponse>;
|
|
46
|
+
requestUpload(params: {
|
|
47
|
+
fileName: string;
|
|
48
|
+
fileType: string;
|
|
49
|
+
fileSize: number;
|
|
50
|
+
}): Promise<UploadRequestResponse>;
|
|
51
|
+
uploadToS3(url: string, content: string, contentType: string): Promise<void>;
|
|
52
|
+
private request;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=api-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAEV,YAAY,EACZ,mBAAmB,EACnB,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,eAAe,EACf,gBAAgB,EAChB,qBAAqB,EACtB,MAAM,YAAY,CAAC;AAEpB,qBAAa,QAAS,SAAQ,KAAK;aAGf,IAAI,EAAE,MAAM;aACZ,MAAM,EAAE,MAAM;gBAF9B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM;CAKjC;AAKD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,MAAM,EAAE,eAAe;IAK7B,QAAQ,CAAC,MAAM,EAAE;QACrB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,CAAC;IAInB,WAAW,CACf,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE;YAAE,EAAE,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACnC,QAAQ,CAAC,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,CAAC;KAChD,GACA,OAAO,CAAC,mBAAmB,CAAC;IAIzB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAIzE,WAAW,IAAI,OAAO,CAAC,eAAe,CAAC;IAIvC,UAAU,CAAC,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQjF,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAIrD,gBAAgB,IAAI,OAAO,CAAC,eAAe,CAAC;IAI5C,YAAY,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAIzC,aAAa,CAAC,MAAM,EAAE;QAC1B,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC,qBAAqB,CAAC;IAI5B,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAYpE,OAAO;CAsCtB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ZephApiClient = exports.ApiError = void 0;
|
|
4
|
+
class ApiError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(message, code, status) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.status = status;
|
|
11
|
+
this.name = 'ApiError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.ApiError = ApiError;
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
16
|
+
const POLL_TIMEOUT_MS = 15_000;
|
|
17
|
+
class ZephApiClient {
|
|
18
|
+
apiKey;
|
|
19
|
+
baseUrl;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.apiKey = config.apiKey;
|
|
22
|
+
this.baseUrl = config.baseUrl;
|
|
23
|
+
}
|
|
24
|
+
async sendPush(params) {
|
|
25
|
+
return this.request('POST', '/pushes/send', params);
|
|
26
|
+
}
|
|
27
|
+
async triggerHook(hookId, params) {
|
|
28
|
+
return this.request('POST', `/hooks/${hookId}/trigger`, params);
|
|
29
|
+
}
|
|
30
|
+
async getHookEvent(hookId, eventId) {
|
|
31
|
+
return this.request('GET', `/hooks/${hookId}/events/${eventId}`, undefined, POLL_TIMEOUT_MS);
|
|
32
|
+
}
|
|
33
|
+
async listDevices() {
|
|
34
|
+
return this.request('GET', '/devices');
|
|
35
|
+
}
|
|
36
|
+
async listPushes(params) {
|
|
37
|
+
const query = new URLSearchParams();
|
|
38
|
+
if (params?.limit)
|
|
39
|
+
query.set('limit', String(params.limit));
|
|
40
|
+
if (params?.type)
|
|
41
|
+
query.set('type', params.type);
|
|
42
|
+
const qs = query.toString();
|
|
43
|
+
return this.request('GET', `/pushes${qs ? `?${qs}` : ''}`);
|
|
44
|
+
}
|
|
45
|
+
async dismissPush(pushId) {
|
|
46
|
+
return this.request('POST', `/pushes/${pushId}/dismiss`);
|
|
47
|
+
}
|
|
48
|
+
async dismissAllPushes() {
|
|
49
|
+
return this.request('POST', '/pushes/dismiss-all');
|
|
50
|
+
}
|
|
51
|
+
async listChannels() {
|
|
52
|
+
return this.request('GET', '/channels');
|
|
53
|
+
}
|
|
54
|
+
async requestUpload(params) {
|
|
55
|
+
return this.request('POST', '/files/upload-request', params);
|
|
56
|
+
}
|
|
57
|
+
async uploadToS3(url, content, contentType) {
|
|
58
|
+
const response = await fetch(url, {
|
|
59
|
+
method: 'PUT',
|
|
60
|
+
headers: { 'Content-Type': contentType },
|
|
61
|
+
body: content,
|
|
62
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new ApiError(`S3 upload failed with status ${response.status}`, 'UPLOAD_FAILED', response.status);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async request(method, path, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
69
|
+
const url = `${this.baseUrl}${path}`;
|
|
70
|
+
const headers = {
|
|
71
|
+
'X-API-Key': this.apiKey,
|
|
72
|
+
};
|
|
73
|
+
if (body) {
|
|
74
|
+
headers['Content-Type'] = 'application/json';
|
|
75
|
+
}
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await fetch(url, {
|
|
79
|
+
method,
|
|
80
|
+
headers,
|
|
81
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
82
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
if (err instanceof DOMException && err.name === 'TimeoutError') {
|
|
87
|
+
throw new ApiError('Request timed out', 'TIMEOUT', 408);
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
let errorBody = null;
|
|
93
|
+
try {
|
|
94
|
+
errorBody = (await response.json());
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Non-JSON error response (e.g., HTML error page)
|
|
98
|
+
}
|
|
99
|
+
const message = errorBody?.error?.message ?? `Request failed with status ${response.status}`;
|
|
100
|
+
const code = errorBody?.error?.code ?? 'UNKNOWN_ERROR';
|
|
101
|
+
throw new ApiError(message, code, response.status);
|
|
102
|
+
}
|
|
103
|
+
return (await response.json());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
exports.ZephApiClient = ZephApiClient;
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,UAAU,QAAO,eAc7B,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadConfig = void 0;
|
|
4
|
+
const DEFAULT_BASE_URL = 'https://api.zeph.to/v1';
|
|
5
|
+
const loadConfig = () => {
|
|
6
|
+
const apiKey = process.env['ZEPH_API_KEY'];
|
|
7
|
+
if (!apiKey) {
|
|
8
|
+
throw new Error('ZEPH_API_KEY environment variable is required. Get one at Settings → API Keys.');
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
apiKey,
|
|
12
|
+
baseUrl: (process.env['ZEPH_BASE_URL'] ?? DEFAULT_BASE_URL).replace(/\/$/, ''),
|
|
13
|
+
hookId: process.env['ZEPH_HOOK_ID'],
|
|
14
|
+
deviceId: process.env['ZEPH_DEVICE_ID'],
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
exports.loadConfig = loadConfig;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import type { ToolError } from './types.js';
|
|
3
|
+
export declare const textResult: (obj: unknown) => CallToolResult;
|
|
4
|
+
export declare const errorResult: (obj: ToolError) => CallToolResult;
|
|
5
|
+
export declare const hookNotConfiguredError: () => CallToolResult;
|
|
6
|
+
export declare const timeoutError: (seconds: number, suggestion: string) => CallToolResult;
|
|
7
|
+
export declare const formatToolError: (err: unknown) => CallToolResult;
|
|
8
|
+
//# sourceMappingURL=error-format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-format.d.ts","sourceRoot":"","sources":["../src/error-format.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,eAAO,MAAM,UAAU,GAAI,KAAK,OAAO,KAAG,cAExC,CAAC;AAEH,eAAO,MAAM,WAAW,GAAI,KAAK,SAAS,KAAG,cAG3C,CAAC;AAEH,eAAO,MAAM,sBAAsB,QAAO,cAKtC,CAAC;AAEL,eAAO,MAAM,YAAY,GAAI,SAAS,MAAM,EAAE,YAAY,MAAM,KAAG,cAK/D,CAAC;AAEL,eAAO,MAAM,eAAe,GAAI,KAAK,OAAO,KAAG,cAsB9C,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatToolError = exports.timeoutError = exports.hookNotConfiguredError = exports.errorResult = exports.textResult = void 0;
|
|
4
|
+
const api_client_js_1 = require("./api-client.js");
|
|
5
|
+
const textResult = (obj) => ({
|
|
6
|
+
content: [{ type: 'text', text: JSON.stringify(obj) }],
|
|
7
|
+
});
|
|
8
|
+
exports.textResult = textResult;
|
|
9
|
+
const errorResult = (obj) => ({
|
|
10
|
+
content: [{ type: 'text', text: JSON.stringify(obj) }],
|
|
11
|
+
isError: true,
|
|
12
|
+
});
|
|
13
|
+
exports.errorResult = errorResult;
|
|
14
|
+
const hookNotConfiguredError = () => (0, exports.errorResult)({
|
|
15
|
+
error: 'HOOK_NOT_CONFIGURED',
|
|
16
|
+
message: 'ZEPH_HOOK_ID environment variable is not set',
|
|
17
|
+
suggestion: 'Create a Hook in Settings → Hooks, then set ZEPH_HOOK_ID in your MCP server config',
|
|
18
|
+
});
|
|
19
|
+
exports.hookNotConfiguredError = hookNotConfiguredError;
|
|
20
|
+
const timeoutError = (seconds, suggestion) => (0, exports.errorResult)({
|
|
21
|
+
error: 'TIMEOUT',
|
|
22
|
+
message: `No response received within ${seconds} seconds`,
|
|
23
|
+
suggestion,
|
|
24
|
+
});
|
|
25
|
+
exports.timeoutError = timeoutError;
|
|
26
|
+
const formatToolError = (err) => {
|
|
27
|
+
const toolError = {
|
|
28
|
+
error: 'UNKNOWN',
|
|
29
|
+
message: err instanceof Error ? err.message : String(err),
|
|
30
|
+
};
|
|
31
|
+
if (err instanceof api_client_js_1.ApiError) {
|
|
32
|
+
toolError.error = err.code;
|
|
33
|
+
toolError.message = err.message;
|
|
34
|
+
if (err.code === 'QUOTA_EXCEEDED') {
|
|
35
|
+
toolError.suggestion = 'Monthly quota exceeded. Upgrade plan for higher limits';
|
|
36
|
+
}
|
|
37
|
+
else if (err.code === 'UNAUTHORIZED') {
|
|
38
|
+
toolError.suggestion = 'Check ZEPH_API_KEY environment variable';
|
|
39
|
+
}
|
|
40
|
+
else if (err.code === 'HOOK_DISABLED') {
|
|
41
|
+
toolError.suggestion = 'Enable the Hook in Settings → Hooks';
|
|
42
|
+
}
|
|
43
|
+
else if (err.code === 'FORBIDDEN') {
|
|
44
|
+
toolError.suggestion = 'Check API key permissions (needs push:write and hook:write)';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return (0, exports.errorResult)(toolError);
|
|
48
|
+
};
|
|
49
|
+
exports.formatToolError = formatToolError;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const config_js_1 = require("./config.js");
|
|
7
|
+
const api_client_js_1 = require("./api-client.js");
|
|
8
|
+
const notify_js_1 = require("./tools/notify.js");
|
|
9
|
+
const prompt_js_1 = require("./tools/prompt.js");
|
|
10
|
+
const input_js_1 = require("./tools/input.js");
|
|
11
|
+
const clipboard_js_1 = require("./tools/clipboard.js");
|
|
12
|
+
const list_js_1 = require("./tools/list.js");
|
|
13
|
+
const dismiss_js_1 = require("./tools/dismiss.js");
|
|
14
|
+
const broadcast_js_1 = require("./tools/broadcast.js");
|
|
15
|
+
const file_js_1 = require("./tools/file.js");
|
|
16
|
+
const devices_js_1 = require("./resources/devices.js");
|
|
17
|
+
const channels_js_1 = require("./resources/channels.js");
|
|
18
|
+
const createServer = () => {
|
|
19
|
+
const config = (0, config_js_1.loadConfig)();
|
|
20
|
+
const client = new api_client_js_1.ZephApiClient(config);
|
|
21
|
+
const server = new mcp_js_1.McpServer({
|
|
22
|
+
name: 'zeph',
|
|
23
|
+
version: '0.2.0',
|
|
24
|
+
}, {
|
|
25
|
+
instructions: [
|
|
26
|
+
'Zeph MCP Server — Send notifications, files, clipboard text, broadcast to channels, manage push history, and interact with users across their devices.',
|
|
27
|
+
'',
|
|
28
|
+
'Available tools:',
|
|
29
|
+
'- zeph_notify: Send push notifications with optional URL (task completion, errors, links)',
|
|
30
|
+
'- zeph_clipboard: Copy text to user\'s device clipboard',
|
|
31
|
+
'- zeph_list: List recent push notifications (history, deduplication)',
|
|
32
|
+
'- zeph_dismiss: Mark a push as read',
|
|
33
|
+
'- zeph_dismiss_all: Clear all notifications',
|
|
34
|
+
'- zeph_broadcast: Send to all subscribers of a channel',
|
|
35
|
+
'- zeph_file: Send a text file (logs, reports, code)',
|
|
36
|
+
'- zeph_prompt: Ask user to choose from options (requires ZEPH_HOOK_ID)',
|
|
37
|
+
'- zeph_input: Request text input from user (requires ZEPH_HOOK_ID)',
|
|
38
|
+
'',
|
|
39
|
+
'Resources:',
|
|
40
|
+
'- zeph://devices: Check which devices are online',
|
|
41
|
+
'- zeph://channels: List available channels for broadcasting',
|
|
42
|
+
].join('\n'),
|
|
43
|
+
});
|
|
44
|
+
(0, notify_js_1.registerNotifyTool)(server, client, config);
|
|
45
|
+
(0, clipboard_js_1.registerClipboardTool)(server, client, config);
|
|
46
|
+
(0, list_js_1.registerListTool)(server, client);
|
|
47
|
+
(0, dismiss_js_1.registerDismissTool)(server, client);
|
|
48
|
+
(0, dismiss_js_1.registerDismissAllTool)(server, client);
|
|
49
|
+
(0, broadcast_js_1.registerBroadcastTool)(server, client);
|
|
50
|
+
(0, file_js_1.registerFileTool)(server, client, config);
|
|
51
|
+
(0, prompt_js_1.registerPromptTool)(server, client, config);
|
|
52
|
+
(0, input_js_1.registerInputTool)(server, client, config);
|
|
53
|
+
(0, devices_js_1.registerDevicesResource)(server, client);
|
|
54
|
+
(0, channels_js_1.registerChannelsResource)(server, client);
|
|
55
|
+
return server;
|
|
56
|
+
};
|
|
57
|
+
const main = async () => {
|
|
58
|
+
const server = createServer();
|
|
59
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
60
|
+
await server.connect(transport);
|
|
61
|
+
console.error('Zeph MCP Server running on stdio');
|
|
62
|
+
};
|
|
63
|
+
main().catch((err) => {
|
|
64
|
+
console.error('Fatal error:', err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
package/dist/poll.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ZephApiClient } from './api-client.js';
|
|
2
|
+
import type { HookEventResponse } from './types.js';
|
|
3
|
+
export interface PollContext {
|
|
4
|
+
_meta?: {
|
|
5
|
+
progressToken?: string | number;
|
|
6
|
+
};
|
|
7
|
+
sendNotification: (notification: any) => Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare const pollForResponse: (client: ZephApiClient, hookId: string, eventId: string, timeoutSeconds: number, ctx: PollContext) => Promise<HookEventResponse | null>;
|
|
10
|
+
//# sourceMappingURL=poll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poll.d.ts","sourceRoot":"","sources":["../src/poll.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAE5C,gBAAgB,EAAE,CAAC,YAAY,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxD;AAOD,eAAO,MAAM,eAAe,GAC1B,QAAQ,aAAa,EACrB,QAAQ,MAAM,EACd,SAAS,MAAM,EACf,gBAAgB,MAAM,EACtB,KAAK,WAAW,KACf,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAsClC,CAAC"}
|
package/dist/poll.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pollForResponse = void 0;
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
const PROGRESS_INTERVAL_SECONDS = 5;
|
|
6
|
+
const pollForResponse = async (client, hookId, eventId, timeoutSeconds, ctx) => {
|
|
7
|
+
const progressToken = ctx._meta?.progressToken;
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
10
|
+
let attempt = 0;
|
|
11
|
+
let lastProgressAt = 0;
|
|
12
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
13
|
+
const event = await client.getHookEvent(hookId, eventId);
|
|
14
|
+
if (event.data.status === 'responded')
|
|
15
|
+
return event;
|
|
16
|
+
if (event.data.status === 'cancelled')
|
|
17
|
+
throw new Error('User cancelled the request');
|
|
18
|
+
if (event.data.status === 'timed_out')
|
|
19
|
+
return null;
|
|
20
|
+
// Throttled progress notification (every 5s)
|
|
21
|
+
if (progressToken !== undefined) {
|
|
22
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
23
|
+
if (elapsed - lastProgressAt >= PROGRESS_INTERVAL_SECONDS) {
|
|
24
|
+
lastProgressAt = elapsed;
|
|
25
|
+
await ctx.sendNotification({
|
|
26
|
+
method: 'notifications/progress',
|
|
27
|
+
params: {
|
|
28
|
+
progressToken,
|
|
29
|
+
progress: elapsed,
|
|
30
|
+
total: timeoutSeconds,
|
|
31
|
+
message: `Waiting for user response... (${elapsed}s / ${timeoutSeconds}s)`,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Adaptive interval: 2s for first 5 attempts, then 3s
|
|
37
|
+
const interval = attempt < 5 ? 2000 : 3000;
|
|
38
|
+
await sleep(interval);
|
|
39
|
+
attempt++;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
exports.pollForResponse = pollForResponse;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"channels.d.ts","sourceRoot":"","sources":["../../src/resources/channels.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,eAAO,MAAM,wBAAwB,GAAI,QAAQ,SAAS,EAAE,QAAQ,aAAa,SAuBhF,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerChannelsResource = void 0;
|
|
4
|
+
const registerChannelsResource = (server, client) => {
|
|
5
|
+
server.registerResource('channels', 'zeph://channels', {
|
|
6
|
+
title: 'Channels',
|
|
7
|
+
description: 'List of channels the user owns or subscribes to. Use to find channelId for broadcasting.',
|
|
8
|
+
mimeType: 'application/json',
|
|
9
|
+
}, async (uri) => {
|
|
10
|
+
try {
|
|
11
|
+
const response = await client.listChannels();
|
|
12
|
+
return {
|
|
13
|
+
contents: [{ uri: uri.href, text: JSON.stringify(response.data, null, 2) }],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch channels';
|
|
18
|
+
return {
|
|
19
|
+
contents: [{ uri: uri.href, text: JSON.stringify({ error: message }) }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
exports.registerChannelsResource = registerChannelsResource;
|