@zyphr-dev/mcp-server 0.1.0 → 0.1.1

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.
@@ -1,222 +0,0 @@
1
- import type { QuickstartChannelMap } from '../quickstart-types.js';
2
-
3
- const DOCS = 'https://docs.zyphr.dev/channels/in-app-messaging';
4
- const ENV: string[] = ['ZYPHR_API_KEY'];
5
- const NEXT_STEPS = [
6
- 'Add ZYPHR_API_KEY to your .env file.',
7
- 'Use the matching subscriberId on the client (e.g. @zyphr-dev/inbox-react) to display the message.',
8
- ];
9
-
10
- export const inboxChannel: QuickstartChannelMap = {
11
- node: {
12
- sdk: {
13
- channel: 'inbox',
14
- language: 'node',
15
- framework: null,
16
- variant: 'sdk',
17
- files: [
18
- {
19
- path: 'src/services/inbox.ts',
20
- purpose: 'Send an in-app inbox message through Zyphr',
21
- contents:
22
- "import { Zyphr } from '@zyphr-dev/node-sdk';\n\n" +
23
- 'const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\n' +
24
- 'export async function notifyReportReady(subscriberId: string, reportId: string) {\n' +
25
- ' return await zyphr.inbox.sendInApp({\n' +
26
- ' subscriberId,\n' +
27
- " title: 'New report ready',\n" +
28
- " body: 'Your report finished processing — click to view.',\n" +
29
- ' actionUrl: `/reports/${reportId}`,\n' +
30
- " actionLabel: 'View report',\n" +
31
- ' });\n' +
32
- '}\n',
33
- overwrite: false,
34
- },
35
- ],
36
- envVarsNeeded: ENV,
37
- nextSteps: NEXT_STEPS,
38
- docsUrl: DOCS,
39
- },
40
- },
41
- python: {
42
- sdk: {
43
- channel: 'inbox',
44
- language: 'python',
45
- framework: null,
46
- variant: 'sdk',
47
- files: [
48
- {
49
- path: 'app/inbox.py',
50
- purpose: 'Send an in-app inbox message through Zyphr',
51
- contents:
52
- 'from .zyphr_client import zyphr_request\n\n' +
53
- 'def notify_report_ready(subscriber_id: str, report_id: str) -> dict:\n' +
54
- ' return zyphr_request("POST", "/inbox", json={\n' +
55
- ' "subscriberId": subscriber_id,\n' +
56
- ' "title": "New report ready",\n' +
57
- ' "body": "Your report finished processing — click to view.",\n' +
58
- ' "actionUrl": f"/reports/{report_id}",\n' +
59
- ' "actionLabel": "View report",\n' +
60
- ' })\n',
61
- overwrite: false,
62
- },
63
- ],
64
- envVarsNeeded: ENV,
65
- nextSteps: NEXT_STEPS,
66
- docsUrl: DOCS,
67
- },
68
- },
69
- ruby: {
70
- sdk: {
71
- channel: 'inbox',
72
- language: 'ruby',
73
- framework: null,
74
- variant: 'sdk',
75
- files: [
76
- {
77
- path: 'app/services/inbox_service.rb',
78
- purpose: 'Send an in-app inbox message through Zyphr',
79
- contents:
80
- 'class InboxService\n' +
81
- ' def self.notify_report_ready(subscriber_id:, report_id:)\n' +
82
- ' Zyphr::InboxApi.new.send_in_app(\n' +
83
- ' Zyphr::SendInAppRequest.new(\n' +
84
- ' subscriber_id: subscriber_id,\n' +
85
- " title: 'New report ready',\n" +
86
- " body: 'Your report finished processing — click to view.',\n" +
87
- " action_url: \"/reports/#{report_id}\",\n" +
88
- " action_label: 'View report'\n" +
89
- ' )\n' +
90
- ' )\n' +
91
- ' end\n' +
92
- 'end\n',
93
- overwrite: false,
94
- },
95
- ],
96
- envVarsNeeded: ENV,
97
- nextSteps: NEXT_STEPS,
98
- docsUrl: DOCS,
99
- },
100
- },
101
- go: {
102
- sdk: {
103
- channel: 'inbox',
104
- language: 'go',
105
- framework: null,
106
- variant: 'sdk',
107
- files: [
108
- {
109
- path: 'internal/notify/inbox.go',
110
- purpose: 'Send an in-app inbox message through Zyphr',
111
- contents:
112
- 'package notify\n\n' +
113
- 'import "yourapp/internal/zyphr"\n\n' +
114
- 'func NotifyReportReady(client *zyphr.Client, subscriberID, reportID string) ([]byte, error) {\n' +
115
- '\treturn client.Do("POST", "/inbox", map[string]any{\n' +
116
- '\t\t"subscriberId": subscriberID,\n' +
117
- '\t\t"title": "New report ready",\n' +
118
- '\t\t"body": "Your report finished processing — click to view.",\n' +
119
- '\t\t"actionUrl": "/reports/" + reportID,\n' +
120
- '\t\t"actionLabel": "View report",\n' +
121
- '\t})\n' +
122
- '}\n',
123
- overwrite: false,
124
- },
125
- ],
126
- envVarsNeeded: ENV,
127
- nextSteps: NEXT_STEPS,
128
- docsUrl: DOCS,
129
- },
130
- },
131
- php: {
132
- sdk: {
133
- channel: 'inbox',
134
- language: 'php',
135
- framework: null,
136
- variant: 'sdk',
137
- files: [
138
- {
139
- path: 'app/Services/InboxService.php',
140
- purpose: 'Send an in-app inbox message through Zyphr',
141
- contents:
142
- '<?php\n\n' +
143
- 'namespace App\\Services;\n\n' +
144
- 'use GuzzleHttp\\Client;\n\n' +
145
- 'class InboxService\n' +
146
- '{\n' +
147
- ' public function notifyReportReady(string $subscriberId, string $reportId): array\n' +
148
- ' {\n' +
149
- " $http = new Client([\n" +
150
- " 'base_uri' => 'https://api.zyphr.dev/v1/',\n" +
151
- " 'headers' => [\n" +
152
- " 'X-API-Key' => getenv('ZYPHR_API_KEY'),\n" +
153
- " 'Content-Type' => 'application/json',\n" +
154
- ' ],\n' +
155
- " ]);\n" +
156
- " $r = $http->post('inbox', ['json' => [\n" +
157
- " 'subscriberId' => $subscriberId,\n" +
158
- " 'title' => 'New report ready',\n" +
159
- " 'body' => 'Your report finished processing — click to view.',\n" +
160
- " 'actionUrl' => \"/reports/{$reportId}\",\n" +
161
- " 'actionLabel' => 'View report',\n" +
162
- ' ]]);\n' +
163
- " return json_decode((string) $r->getBody(), true);\n" +
164
- ' }\n' +
165
- '}\n',
166
- overwrite: false,
167
- },
168
- ],
169
- envVarsNeeded: ENV,
170
- nextSteps: NEXT_STEPS,
171
- docsUrl: DOCS,
172
- },
173
- },
174
- csharp: {
175
- sdk: {
176
- channel: 'inbox',
177
- language: 'csharp',
178
- framework: null,
179
- variant: 'sdk',
180
- files: [
181
- {
182
- path: 'Services/InboxService.cs',
183
- purpose: 'Send an in-app inbox message through Zyphr',
184
- contents:
185
- 'using ZyphrDev.SDK.Api;\n' +
186
- 'using ZyphrDev.SDK.Client;\n' +
187
- 'using ZyphrDev.SDK.Model;\n\n' +
188
- 'namespace YourApp.Services;\n\n' +
189
- 'public class InboxService\n' +
190
- '{\n' +
191
- ' private readonly InboxApi _inbox;\n' +
192
- ' public InboxService()\n' +
193
- ' {\n' +
194
- ' var config = new Configuration\n' +
195
- ' {\n' +
196
- ' ApiKey = new Dictionary<string, string>\n' +
197
- ' {\n' +
198
- ' { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n' +
199
- ' }\n' +
200
- ' };\n' +
201
- ' _inbox = new InboxApi(config);\n' +
202
- ' }\n\n' +
203
- ' public Task<SendInAppResponse> NotifyReportReadyAsync(string subscriberId, string reportId)\n' +
204
- ' {\n' +
205
- ' return _inbox.SendInAppAsync(new SendInAppRequest(\n' +
206
- ' subscriberId: subscriberId,\n' +
207
- ' title: "New report ready",\n' +
208
- ' body: "Your report finished processing — click to view.",\n' +
209
- ' actionUrl: $"/reports/{reportId}",\n' +
210
- ' actionLabel: "View report"\n' +
211
- ' ));\n' +
212
- ' }\n' +
213
- '}\n',
214
- overwrite: false,
215
- },
216
- ],
217
- envVarsNeeded: ENV,
218
- nextSteps: NEXT_STEPS,
219
- docsUrl: DOCS,
220
- },
221
- },
222
- };
@@ -1,45 +0,0 @@
1
- import type { SdkLanguage, QuickstartChannel } from '../../schemas.js';
2
- import type { QuickstartChannelMap, QuickstartResult } from '../quickstart-types.js';
3
- import { emailChannel } from './email.js';
4
- import { inboxChannel } from './inbox.js';
5
- import { pushChannel } from './push.js';
6
- import { smsChannel } from './sms.js';
7
- import { webhookChannel } from './webhook.js';
8
-
9
- const REGISTRY: Record<QuickstartChannel, QuickstartChannelMap> = {
10
- email: emailChannel,
11
- push: pushChannel,
12
- sms: smsChannel,
13
- inbox: inboxChannel,
14
- webhook: webhookChannel,
15
- };
16
-
17
- export interface ResolveQuickstartArgs {
18
- channel: QuickstartChannel;
19
- language: SdkLanguage;
20
- framework?: string;
21
- }
22
-
23
- export interface ResolveQuickstartResult {
24
- result: QuickstartResult;
25
- frameworkRecognized: boolean;
26
- }
27
-
28
- export function resolveQuickstart(
29
- args: ResolveQuickstartArgs,
30
- ): ResolveQuickstartResult | null {
31
- const langMap = REGISTRY[args.channel]?.[args.language];
32
- if (!langMap) return null;
33
-
34
- if (args.framework) {
35
- const key = args.framework.trim().toLowerCase();
36
- const fw = langMap.frameworks?.[key];
37
- if (fw) return { result: fw, frameworkRecognized: true };
38
- // unknown framework → fall back to plain SDK variant, signal recognition status
39
- return { result: langMap.sdk, frameworkRecognized: false };
40
- }
41
-
42
- return { result: langMap.sdk, frameworkRecognized: true };
43
- }
44
-
45
- export const QUICKSTART_REGISTRY = REGISTRY;
@@ -1,216 +0,0 @@
1
- import type { QuickstartChannelMap } from '../quickstart-types.js';
2
-
3
- const DOCS = 'https://docs.zyphr.dev/channels/push-notifications';
4
- const ENV: string[] = ['ZYPHR_API_KEY'];
5
- const NEXT_STEPS = [
6
- 'Add ZYPHR_API_KEY to your .env file.',
7
- 'Register at least one device for the target subscriber (via the SDK or dashboard) before sending.',
8
- 'Verify your push provider credentials (APNs/FCM) are configured in the Zyphr dashboard.',
9
- ];
10
-
11
- export const pushChannel: QuickstartChannelMap = {
12
- node: {
13
- sdk: {
14
- channel: 'push',
15
- language: 'node',
16
- framework: null,
17
- variant: 'sdk',
18
- files: [
19
- {
20
- path: 'src/services/push.ts',
21
- purpose: 'Send a push notification through Zyphr',
22
- contents:
23
- "import { Zyphr } from '@zyphr-dev/node-sdk';\n\n" +
24
- 'const zyphr = new Zyphr({ apiKey: process.env.ZYPHR_API_KEY! });\n\n' +
25
- 'export async function pushOrderShipped(subscriberId: string, orderId: string) {\n' +
26
- ' return await zyphr.push.sendPush({\n' +
27
- ' subscriberId,\n' +
28
- " title: 'Order shipped',\n" +
29
- ' body: `Order ${orderId} is on its way.`,\n' +
30
- ' data: { orderId },\n' +
31
- ' });\n' +
32
- '}\n',
33
- overwrite: false,
34
- },
35
- ],
36
- envVarsNeeded: ENV,
37
- nextSteps: NEXT_STEPS,
38
- docsUrl: DOCS,
39
- },
40
- },
41
- python: {
42
- sdk: {
43
- channel: 'push',
44
- language: 'python',
45
- framework: null,
46
- variant: 'sdk',
47
- files: [
48
- {
49
- path: 'app/push.py',
50
- purpose: 'Send a push notification through Zyphr',
51
- contents:
52
- 'from .zyphr_client import zyphr_request\n\n' +
53
- 'def push_order_shipped(subscriber_id: str, order_id: str) -> dict:\n' +
54
- ' return zyphr_request("POST", "/push", json={\n' +
55
- ' "subscriberId": subscriber_id,\n' +
56
- ' "title": "Order shipped",\n' +
57
- ' "body": f"Order {order_id} is on its way.",\n' +
58
- ' "data": {"orderId": order_id},\n' +
59
- ' })\n',
60
- overwrite: false,
61
- },
62
- ],
63
- envVarsNeeded: ENV,
64
- nextSteps: NEXT_STEPS,
65
- docsUrl: DOCS,
66
- },
67
- },
68
- ruby: {
69
- sdk: {
70
- channel: 'push',
71
- language: 'ruby',
72
- framework: null,
73
- variant: 'sdk',
74
- files: [
75
- {
76
- path: 'app/services/push_service.rb',
77
- purpose: 'Send a push notification through Zyphr',
78
- contents:
79
- 'class PushService\n' +
80
- ' def self.order_shipped(subscriber_id:, order_id:)\n' +
81
- ' Zyphr::PushApi.new.send_push(\n' +
82
- ' Zyphr::SendPushRequest.new(\n' +
83
- ' subscriber_id: subscriber_id,\n' +
84
- " title: 'Order shipped',\n" +
85
- " body: \"Order #{order_id} is on its way.\",\n" +
86
- ' data: { orderId: order_id }\n' +
87
- ' )\n' +
88
- ' )\n' +
89
- ' end\n' +
90
- 'end\n',
91
- overwrite: false,
92
- },
93
- ],
94
- envVarsNeeded: ENV,
95
- nextSteps: NEXT_STEPS,
96
- docsUrl: DOCS,
97
- },
98
- },
99
- go: {
100
- sdk: {
101
- channel: 'push',
102
- language: 'go',
103
- framework: null,
104
- variant: 'sdk',
105
- files: [
106
- {
107
- path: 'internal/notify/push.go',
108
- purpose: 'Send a push notification through Zyphr',
109
- contents:
110
- 'package notify\n\n' +
111
- 'import "yourapp/internal/zyphr"\n\n' +
112
- 'func PushOrderShipped(client *zyphr.Client, subscriberID, orderID string) ([]byte, error) {\n' +
113
- '\treturn client.Do("POST", "/push", map[string]any{\n' +
114
- '\t\t"subscriberId": subscriberID,\n' +
115
- '\t\t"title": "Order shipped",\n' +
116
- '\t\t"body": "Order " + orderID + " is on its way.",\n' +
117
- '\t\t"data": map[string]string{"orderId": orderID},\n' +
118
- '\t})\n' +
119
- '}\n',
120
- overwrite: false,
121
- },
122
- ],
123
- envVarsNeeded: ENV,
124
- nextSteps: NEXT_STEPS,
125
- docsUrl: DOCS,
126
- },
127
- },
128
- php: {
129
- sdk: {
130
- channel: 'push',
131
- language: 'php',
132
- framework: null,
133
- variant: 'sdk',
134
- files: [
135
- {
136
- path: 'app/Services/PushService.php',
137
- purpose: 'Send a push notification through Zyphr',
138
- contents:
139
- '<?php\n\n' +
140
- 'namespace App\\Services;\n\n' +
141
- 'use GuzzleHttp\\Client;\n\n' +
142
- 'class PushService\n' +
143
- '{\n' +
144
- ' public function orderShipped(string $subscriberId, string $orderId): array\n' +
145
- ' {\n' +
146
- " $http = new Client([\n" +
147
- " 'base_uri' => 'https://api.zyphr.dev/v1/',\n" +
148
- " 'headers' => [\n" +
149
- " 'X-API-Key' => getenv('ZYPHR_API_KEY'),\n" +
150
- " 'Content-Type' => 'application/json',\n" +
151
- ' ],\n' +
152
- " ]);\n" +
153
- " $r = $http->post('push', ['json' => [\n" +
154
- " 'subscriberId' => $subscriberId,\n" +
155
- " 'title' => 'Order shipped',\n" +
156
- " 'body' => \"Order {$orderId} is on its way.\",\n" +
157
- " 'data' => ['orderId' => $orderId],\n" +
158
- ' ]]);\n' +
159
- " return json_decode((string) $r->getBody(), true);\n" +
160
- ' }\n' +
161
- '}\n',
162
- overwrite: false,
163
- },
164
- ],
165
- envVarsNeeded: ENV,
166
- nextSteps: NEXT_STEPS,
167
- docsUrl: DOCS,
168
- },
169
- },
170
- csharp: {
171
- sdk: {
172
- channel: 'push',
173
- language: 'csharp',
174
- framework: null,
175
- variant: 'sdk',
176
- files: [
177
- {
178
- path: 'Services/PushService.cs',
179
- purpose: 'Send a push notification through Zyphr',
180
- contents:
181
- 'using ZyphrDev.SDK.Api;\n' +
182
- 'using ZyphrDev.SDK.Client;\n' +
183
- 'using ZyphrDev.SDK.Model;\n\n' +
184
- 'namespace YourApp.Services;\n\n' +
185
- 'public class PushService\n' +
186
- '{\n' +
187
- ' private readonly PushApi _push;\n' +
188
- ' public PushService()\n' +
189
- ' {\n' +
190
- ' var config = new Configuration\n' +
191
- ' {\n' +
192
- ' ApiKey = new Dictionary<string, string>\n' +
193
- ' {\n' +
194
- ' { "X-API-Key", Environment.GetEnvironmentVariable("ZYPHR_API_KEY")! }\n' +
195
- ' }\n' +
196
- ' };\n' +
197
- ' _push = new PushApi(config);\n' +
198
- ' }\n\n' +
199
- ' public Task<SendPushResponse> OrderShippedAsync(string subscriberId, string orderId)\n' +
200
- ' {\n' +
201
- ' return _push.SendPushAsync(new SendPushRequest(\n' +
202
- ' subscriberId: subscriberId,\n' +
203
- ' title: "Order shipped",\n' +
204
- ' body: $"Order {orderId} is on its way."\n' +
205
- ' ));\n' +
206
- ' }\n' +
207
- '}\n',
208
- overwrite: false,
209
- },
210
- ],
211
- envVarsNeeded: ENV,
212
- nextSteps: NEXT_STEPS,
213
- docsUrl: DOCS,
214
- },
215
- },
216
- };
@@ -1,108 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import {
3
- quickstartChannels,
4
- sdkLanguages,
5
- type QuickstartChannel,
6
- type SdkLanguage,
7
- } from '../../schemas.js';
8
- import { resolveQuickstart, QUICKSTART_REGISTRY } from './index.js';
9
-
10
- const ALL_PAIRS = quickstartChannels.flatMap((c) =>
11
- sdkLanguages.map((l) => [c, l] as [QuickstartChannel, SdkLanguage]),
12
- );
13
-
14
- const SCOPED_FRAMEWORKS: Array<[QuickstartChannel, SdkLanguage, string]> = [
15
- ['email', 'node', 'express'],
16
- ['email', 'node', 'nextjs'],
17
- ['email', 'python', 'flask'],
18
- ['email', 'python', 'fastapi'],
19
- ['email', 'ruby', 'rails'],
20
- ['email', 'php', 'laravel'],
21
- ['email', 'csharp', 'aspnetcore'],
22
- ['webhook', 'node', 'express'],
23
- ['webhook', 'node', 'nextjs'],
24
- ['webhook', 'python', 'flask'],
25
- ['webhook', 'python', 'fastapi'],
26
- ['webhook', 'ruby', 'rails'],
27
- ['webhook', 'php', 'laravel'],
28
- ['webhook', 'csharp', 'aspnetcore'],
29
- ];
30
-
31
- const VERIFY_PATTERN = /verify|VerifyWebhook|verify_zyphr_webhook|verifyZyphrWebhook|FixedTimeEquals|hash_equals|secure_compare|timingSafeEqual|compare_digest|hmac\.Equal/;
32
-
33
- describe('QUICKSTART_REGISTRY', () => {
34
- it.each(ALL_PAIRS)('returns at least one file for channel=%s language=%s', (channel, language) => {
35
- const entry = QUICKSTART_REGISTRY[channel][language];
36
- expect(entry).toBeDefined();
37
- expect(entry!.sdk.files.length).toBeGreaterThanOrEqual(1);
38
- for (const f of entry!.sdk.files) {
39
- expect(f.path).toMatch(/\S/);
40
- expect(f.contents).toMatch(/\S/);
41
- expect(f.contents.length).toBeGreaterThan(40);
42
- }
43
- });
44
-
45
- it.each(ALL_PAIRS)('declares ZYPHR_* env vars for channel=%s language=%s', (channel, language) => {
46
- const entry = QUICKSTART_REGISTRY[channel][language]!.sdk;
47
- expect(entry.envVarsNeeded.some((v) => v.startsWith('ZYPHR_'))).toBe(true);
48
- if (channel === 'webhook') {
49
- expect(entry.envVarsNeeded).toContain('ZYPHR_WEBHOOK_SECRET');
50
- } else {
51
- expect(entry.envVarsNeeded).toContain('ZYPHR_API_KEY');
52
- }
53
- });
54
-
55
- it.each(sdkLanguages.map((l) => [l]))(
56
- 'webhook handler for %s verifies the signature (safety AC)',
57
- (language) => {
58
- const entry = QUICKSTART_REGISTRY.webhook[language]!.sdk;
59
- const allCode = entry.files.map((f) => f.contents).join('\n');
60
- expect(allCode).toMatch(VERIFY_PATTERN);
61
- },
62
- );
63
-
64
- it.each(SCOPED_FRAMEWORKS)(
65
- 'has a framework variant for channel=%s language=%s framework=%s',
66
- (channel, language, framework) => {
67
- const fw = QUICKSTART_REGISTRY[channel][language]!.frameworks?.[framework];
68
- expect(fw).toBeDefined();
69
- expect(fw!.framework).toBe(framework);
70
- expect(fw!.files.length).toBeGreaterThanOrEqual(1);
71
- },
72
- );
73
-
74
- it('webhook framework variants ALL verify the signature (safety AC)', () => {
75
- for (const [, language, framework] of SCOPED_FRAMEWORKS.filter(([c]) => c === 'webhook')) {
76
- const fw = QUICKSTART_REGISTRY.webhook[language]!.frameworks?.[framework];
77
- expect(fw, `missing webhook framework ${language}/${framework}`).toBeDefined();
78
- const code = fw!.files.map((f) => f.contents).join('\n');
79
- expect(code, `webhook ${language}/${framework} must verify`).toMatch(VERIFY_PATTERN);
80
- }
81
- });
82
- });
83
-
84
- describe('resolveQuickstart', () => {
85
- it('returns plain SDK variant when no framework is provided', () => {
86
- const r = resolveQuickstart({ channel: 'email', language: 'node' });
87
- expect(r).not.toBeNull();
88
- expect(r!.frameworkRecognized).toBe(true);
89
- expect(r!.result.framework).toBeNull();
90
- });
91
-
92
- it('returns framework variant when recognized (case-insensitive)', () => {
93
- const r = resolveQuickstart({ channel: 'email', language: 'node', framework: 'EXPRESS' });
94
- expect(r!.frameworkRecognized).toBe(true);
95
- expect(r!.result.framework).toBe('express');
96
- });
97
-
98
- it('falls back to plain SDK when framework is unrecognized', () => {
99
- const r = resolveQuickstart({ channel: 'email', language: 'node', framework: 'lol-no' });
100
- expect(r!.frameworkRecognized).toBe(false);
101
- expect(r!.result.framework).toBeNull();
102
- });
103
-
104
- it('returns the webhook-handler variant for the webhook channel', () => {
105
- const r = resolveQuickstart({ channel: 'webhook', language: 'python' });
106
- expect(r!.result.variant).toBe('webhook-handler');
107
- });
108
- });