@meetlark/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.
@@ -0,0 +1,265 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
5
+ import { createMeetLarkServer } from "../src/server.ts";
6
+
7
+ let originalFetch: typeof globalThis.fetch;
8
+ let client: Client;
9
+
10
+ beforeEach(async () => {
11
+ originalFetch = globalThis.fetch;
12
+ });
13
+
14
+ afterEach(async () => {
15
+ globalThis.fetch = originalFetch;
16
+ await client?.close();
17
+ });
18
+
19
+ async function setupServer() {
20
+ const server = createMeetLarkServer("https://api.test");
21
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
22
+ client = new Client({ name: "test-client", version: "1.0.0" });
23
+ await Promise.all([
24
+ client.connect(clientTransport),
25
+ server.connect(serverTransport),
26
+ ]);
27
+ return client;
28
+ }
29
+
30
+ function mockFetch(response: unknown, status = 200) {
31
+ let capturedUrl = "";
32
+ let capturedInit: RequestInit | undefined;
33
+
34
+ globalThis.fetch = async (input, init) => {
35
+ capturedUrl = String(input);
36
+ capturedInit = init;
37
+ return new Response(JSON.stringify(response), { status });
38
+ };
39
+
40
+ return { getCapturedUrl: () => capturedUrl, getCapturedInit: () => capturedInit };
41
+ }
42
+
43
+ describe("meetlark_create_poll", () => {
44
+ it("maps inputs to correct API call", async () => {
45
+ const captured = mockFetch(
46
+ { data: { id: "poll_1", adminToken: "adm_1", adminUrl: "http://test/admin/adm_1", participateToken: "prt_1", participateUrl: "http://test/vote/participate/prt_1" }, meta: { requestId: "r1" } },
47
+ 201,
48
+ );
49
+
50
+ const c = await setupServer();
51
+ await c.callTool({ name: "meetlark_create_poll", arguments: {
52
+ title: "Standup",
53
+ description: "Daily standup",
54
+ timeSlots: [{ date: "2026-02-10", startTime: "09:00", endTime: "09:30" }],
55
+ creatorEmail: "test@example.com",
56
+ creatorName: "Test User",
57
+ }});
58
+
59
+ assert.ok(captured.getCapturedUrl().includes("/api/v1/polls?autoVerify=true"));
60
+ assert.strictEqual(captured.getCapturedInit()?.method, "POST");
61
+ const body = JSON.parse(captured.getCapturedInit()?.body as string);
62
+ assert.strictEqual(body.title, "Standup");
63
+ assert.strictEqual(body.creatorEmail, "test@example.com");
64
+ });
65
+
66
+ it("formats success response as MCP result", async () => {
67
+ const responseData = {
68
+ id: "poll_1",
69
+ adminToken: "adm_abc",
70
+ adminUrl: "http://test/admin/adm_abc",
71
+ participateToken: "prt_abc",
72
+ participateUrl: "http://test/vote/participate/prt_abc",
73
+ };
74
+
75
+ mockFetch({ data: responseData, meta: { requestId: "r1" } }, 201);
76
+
77
+ const c = await setupServer();
78
+ const result = await c.callTool({ name: "meetlark_create_poll", arguments: {
79
+ title: "Test",
80
+ timeSlots: [{ date: "2026-02-10", startTime: "09:00", endTime: "09:30" }],
81
+ creatorEmail: "test@example.com",
82
+ }});
83
+
84
+ assert.strictEqual(result.isError, undefined);
85
+ const content = result.content as Array<{ type: string; text: string }>;
86
+ assert.strictEqual(content[0].type, "text");
87
+ const parsed = JSON.parse(content[0].text);
88
+ assert.strictEqual(parsed.adminToken, "adm_abc");
89
+ assert.strictEqual(parsed.participateUrl, "http://test/vote/participate/prt_abc");
90
+ });
91
+
92
+ it("returns verification message on 403", async () => {
93
+ mockFetch(
94
+ {
95
+ error: { code: "email_not_verified", message: "Email not verified", details: { verificationSent: true, email: "test@example.com" } },
96
+ meta: { requestId: "r1" },
97
+ },
98
+ 403,
99
+ );
100
+
101
+ const c = await setupServer();
102
+ const result = await c.callTool({ name: "meetlark_create_poll", arguments: {
103
+ title: "Test",
104
+ timeSlots: [{ date: "2026-02-10", startTime: "09:00", endTime: "09:30" }],
105
+ creatorEmail: "test@example.com",
106
+ }});
107
+
108
+ assert.strictEqual(result.isError, true);
109
+ const content = result.content as Array<{ type: string; text: string }>;
110
+ assert.ok(content[0].text.includes("Email verification required"));
111
+ assert.ok(content[0].text.includes("test@example.com"));
112
+ });
113
+ });
114
+
115
+ describe("meetlark_vote", () => {
116
+ it("maps inputs to correct API call", async () => {
117
+ const captured = mockFetch(
118
+ { data: { success: true, participant: { id: "p1", name: "Alice", votedAt: "2026-02-01T14:00:00Z" } }, meta: { requestId: "r1" } },
119
+ );
120
+
121
+ const c = await setupServer();
122
+ await c.callTool({ name: "meetlark_vote", arguments: {
123
+ participateToken: "prt_abc123",
124
+ name: "Alice",
125
+ votes: [{ timeSlotId: "ts1", value: "yes" }],
126
+ }});
127
+
128
+ assert.strictEqual(captured.getCapturedUrl(), "https://api.test/api/v1/vote/participate/prt_abc123");
129
+ assert.strictEqual(captured.getCapturedInit()?.method, "POST");
130
+ const body = JSON.parse(captured.getCapturedInit()?.body as string);
131
+ assert.strictEqual(body.name, "Alice");
132
+ assert.deepStrictEqual(body.votes, [{ timeSlotId: "ts1", value: "yes" }]);
133
+ });
134
+
135
+ it("returns success result", async () => {
136
+ const responseData = { success: true, participant: { id: "p1", name: "Alice", votedAt: "2026-02-01T14:00:00Z" } };
137
+ mockFetch({ data: responseData, meta: { requestId: "r1" } });
138
+
139
+ const c = await setupServer();
140
+ const result = await c.callTool({ name: "meetlark_vote", arguments: {
141
+ participateToken: "prt_abc123",
142
+ name: "Alice",
143
+ votes: [{ timeSlotId: "ts1", value: "yes" }],
144
+ }});
145
+
146
+ assert.strictEqual(result.isError, undefined);
147
+ const content = result.content as Array<{ type: string; text: string }>;
148
+ const parsed = JSON.parse(content[0].text);
149
+ assert.strictEqual(parsed.success, true);
150
+ assert.strictEqual(parsed.participant.name, "Alice");
151
+ });
152
+ });
153
+
154
+ describe("meetlark_get_results", () => {
155
+ it("calls correct endpoint", async () => {
156
+ const captured = mockFetch(
157
+ { data: { poll: { id: "poll_1", title: "Test", status: "open", timeSlots: [] } }, meta: { requestId: "r1" } },
158
+ );
159
+
160
+ const c = await setupServer();
161
+ await c.callTool({ name: "meetlark_get_results", arguments: {
162
+ participateToken: "prt_abc123",
163
+ }});
164
+
165
+ assert.strictEqual(captured.getCapturedUrl(), "https://api.test/api/v1/vote/participate/prt_abc123");
166
+ assert.strictEqual(captured.getCapturedInit()?.method, undefined); // GET
167
+ });
168
+
169
+ it("returns poll data", async () => {
170
+ const pollData = { poll: { id: "poll_1", title: "Standup", status: "open", timeSlots: [{ id: "ts1", date: "2026-02-10", startTime: "09:00", endTime: "09:30" }] } };
171
+ mockFetch({ data: pollData, meta: { requestId: "r1" } });
172
+
173
+ const c = await setupServer();
174
+ const result = await c.callTool({ name: "meetlark_get_results", arguments: {
175
+ participateToken: "prt_abc123",
176
+ }});
177
+
178
+ assert.strictEqual(result.isError, undefined);
179
+ const content = result.content as Array<{ type: string; text: string }>;
180
+ const parsed = JSON.parse(content[0].text);
181
+ assert.strictEqual(parsed.poll.title, "Standup");
182
+ assert.strictEqual(parsed.poll.timeSlots.length, 1);
183
+ });
184
+ });
185
+
186
+ describe("meetlark_get_admin_view", () => {
187
+ it("calls correct endpoint", async () => {
188
+ const captured = mockFetch(
189
+ { data: { id: "poll_1", title: "Test", status: "open" }, meta: { requestId: "r1" } },
190
+ );
191
+
192
+ const c = await setupServer();
193
+ await c.callTool({ name: "meetlark_get_admin_view", arguments: {
194
+ adminToken: "adm_abc123",
195
+ }});
196
+
197
+ assert.strictEqual(captured.getCapturedUrl(), "https://api.test/api/v1/polls/admin/adm_abc123");
198
+ assert.strictEqual(captured.getCapturedInit()?.method, undefined); // GET
199
+ });
200
+ });
201
+
202
+ describe("meetlark_close_poll", () => {
203
+ it("calls correct endpoint", async () => {
204
+ const captured = mockFetch(
205
+ { data: { success: true }, meta: { requestId: "r1" } },
206
+ );
207
+
208
+ const c = await setupServer();
209
+ await c.callTool({ name: "meetlark_close_poll", arguments: {
210
+ adminToken: "adm_abc123",
211
+ }});
212
+
213
+ assert.strictEqual(captured.getCapturedUrl(), "https://api.test/api/v1/polls/admin/adm_abc123/close");
214
+ assert.strictEqual(captured.getCapturedInit()?.method, "POST");
215
+ });
216
+ });
217
+
218
+ describe("meetlark_reopen_poll", () => {
219
+ it("calls correct endpoint", async () => {
220
+ const captured = mockFetch(
221
+ { data: { success: true }, meta: { requestId: "r1" } },
222
+ );
223
+
224
+ const c = await setupServer();
225
+ await c.callTool({ name: "meetlark_reopen_poll", arguments: {
226
+ adminToken: "adm_abc123",
227
+ }});
228
+
229
+ assert.strictEqual(captured.getCapturedUrl(), "https://api.test/api/v1/polls/admin/adm_abc123/reopen");
230
+ assert.strictEqual(captured.getCapturedInit()?.method, "POST");
231
+ });
232
+ });
233
+
234
+ describe("error handling", () => {
235
+ it("tools return isError on API errors", async () => {
236
+ mockFetch(
237
+ { error: { code: "poll_not_found", message: "Poll not found" }, meta: { requestId: "r1" } },
238
+ 404,
239
+ );
240
+
241
+ const c = await setupServer();
242
+ const result = await c.callTool({ name: "meetlark_close_poll", arguments: {
243
+ adminToken: "adm_invalid",
244
+ }});
245
+
246
+ assert.strictEqual(result.isError, true);
247
+ const content = result.content as Array<{ type: string; text: string }>;
248
+ assert.ok(content[0].text.includes("Poll not found"));
249
+ });
250
+
251
+ it("tools return isError on network errors", async () => {
252
+ globalThis.fetch = async () => {
253
+ throw new Error("Connection refused");
254
+ };
255
+
256
+ const c = await setupServer();
257
+ const result = await c.callTool({ name: "meetlark_get_results", arguments: {
258
+ participateToken: "prt_abc123",
259
+ }});
260
+
261
+ assert.strictEqual(result.isError, true);
262
+ const content = result.content as Array<{ type: string; text: string }>;
263
+ assert.ok(content[0].text.includes("Could not connect to MeetLark API"));
264
+ });
265
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "declaration": true,
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "isolatedModules": true
12
+ },
13
+ "include": ["src/**/*.ts", "bin/**/*.ts"],
14
+ "exclude": ["node_modules", "dist"]
15
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts", "bin/cli.ts"],
5
+ format: ["esm"],
6
+ dts: true,
7
+ clean: true,
8
+ });