@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,44 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const cliPath = resolve(__dirname, "..", "bin", "cli.ts");
9
+
10
+ describe("cli entry point", () => {
11
+ it("file exists and is readable", () => {
12
+ const content = readFileSync(cliPath, "utf-8");
13
+ assert.ok(content.length > 0, "cli.ts should not be empty");
14
+ });
15
+
16
+ it("has shebang line", () => {
17
+ const content = readFileSync(cliPath, "utf-8");
18
+ assert.ok(content.startsWith("#!/usr/bin/env node"), "cli.ts should start with shebang");
19
+ });
20
+
21
+ it("imports StdioServerTransport", () => {
22
+ const content = readFileSync(cliPath, "utf-8");
23
+ assert.ok(
24
+ content.includes("StdioServerTransport"),
25
+ "cli.ts should import StdioServerTransport",
26
+ );
27
+ });
28
+
29
+ it("imports createMeetLarkServer", () => {
30
+ const content = readFileSync(cliPath, "utf-8");
31
+ assert.ok(
32
+ content.includes("createMeetLarkServer"),
33
+ "cli.ts should import createMeetLarkServer",
34
+ );
35
+ });
36
+
37
+ it("connects server to transport", () => {
38
+ const content = readFileSync(cliPath, "utf-8");
39
+ assert.ok(
40
+ content.includes("server.connect(transport)"),
41
+ "cli.ts should connect server to transport",
42
+ );
43
+ });
44
+ });
@@ -0,0 +1,234 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { MeetLarkClient } from "../src/client.ts";
4
+
5
+ let originalFetch: typeof globalThis.fetch;
6
+
7
+ beforeEach(() => {
8
+ originalFetch = globalThis.fetch;
9
+ });
10
+
11
+ afterEach(() => {
12
+ globalThis.fetch = originalFetch;
13
+ });
14
+
15
+ describe("MeetLarkClient", () => {
16
+ it("createPoll sends POST with autoVerify=true", async () => {
17
+ let capturedUrl = "";
18
+ let capturedInit: RequestInit | undefined;
19
+
20
+ globalThis.fetch = async (input, init) => {
21
+ capturedUrl = String(input);
22
+ capturedInit = init;
23
+ return new Response(
24
+ JSON.stringify({ data: { id: "poll_1" }, meta: { requestId: "r1" } }),
25
+ { status: 201 },
26
+ );
27
+ };
28
+
29
+ const client = new MeetLarkClient("https://api.test");
30
+ await client.createPoll({ title: "Test" });
31
+
32
+ assert.ok(capturedUrl.includes("?autoVerify=true"));
33
+ assert.strictEqual(capturedInit?.method, "POST");
34
+ });
35
+
36
+ it("createPoll returns parsed response on success", async () => {
37
+ const responseData = {
38
+ id: "poll_1",
39
+ adminToken: "adm_abc",
40
+ participateToken: "prt_abc",
41
+ };
42
+
43
+ globalThis.fetch = async () => {
44
+ return new Response(
45
+ JSON.stringify({ data: responseData, meta: { requestId: "r1" } }),
46
+ { status: 201 },
47
+ );
48
+ };
49
+
50
+ const client = new MeetLarkClient("https://api.test");
51
+ const result = await client.createPoll({ title: "Test" });
52
+
53
+ assert.deepStrictEqual(result.data, responseData);
54
+ assert.strictEqual(result.error, undefined);
55
+ });
56
+
57
+ it("createPoll returns verification error on 403", async () => {
58
+ globalThis.fetch = async () => {
59
+ return new Response(
60
+ JSON.stringify({
61
+ error: {
62
+ code: "email_not_verified",
63
+ message: "Email not verified",
64
+ details: { verificationSent: true, email: "test@example.com" },
65
+ },
66
+ meta: { requestId: "r1" },
67
+ }),
68
+ { status: 403 },
69
+ );
70
+ };
71
+
72
+ const client = new MeetLarkClient("https://api.test");
73
+ const result = await client.createPoll({ title: "Test" });
74
+
75
+ assert.strictEqual(result.error?.status, 403);
76
+ assert.strictEqual(result.error?.code, "email_not_verified");
77
+ assert.strictEqual(result.error?.message, "Email not verified");
78
+ assert.strictEqual(result.data, undefined);
79
+ });
80
+
81
+ it("getResults sends GET to correct path", async () => {
82
+ let capturedUrl = "";
83
+ let capturedInit: RequestInit | undefined;
84
+
85
+ globalThis.fetch = async (input, init) => {
86
+ capturedUrl = String(input);
87
+ capturedInit = init;
88
+ return new Response(
89
+ JSON.stringify({
90
+ data: { poll: { id: "poll_1" } },
91
+ meta: { requestId: "r1" },
92
+ }),
93
+ { status: 200 },
94
+ );
95
+ };
96
+
97
+ const client = new MeetLarkClient("https://api.test");
98
+ await client.getResults("prt_abc123");
99
+
100
+ assert.strictEqual(
101
+ capturedUrl,
102
+ "https://api.test/api/v1/vote/participate/prt_abc123",
103
+ );
104
+ assert.strictEqual(capturedInit?.method, undefined); // GET is default
105
+ });
106
+
107
+ it("vote sends POST with body", async () => {
108
+ let capturedUrl = "";
109
+ let capturedInit: RequestInit | undefined;
110
+
111
+ globalThis.fetch = async (input, init) => {
112
+ capturedUrl = String(input);
113
+ capturedInit = init;
114
+ return new Response(
115
+ JSON.stringify({
116
+ data: { success: true, participant: { id: "p1" } },
117
+ meta: { requestId: "r1" },
118
+ }),
119
+ { status: 200 },
120
+ );
121
+ };
122
+
123
+ const voteData = { name: "Alice", votes: [{ timeSlotId: "ts1", value: "yes" }] };
124
+ const client = new MeetLarkClient("https://api.test");
125
+ await client.vote("prt_abc123", voteData);
126
+
127
+ assert.strictEqual(
128
+ capturedUrl,
129
+ "https://api.test/api/v1/vote/participate/prt_abc123",
130
+ );
131
+ assert.strictEqual(capturedInit?.method, "POST");
132
+ assert.strictEqual(capturedInit?.body, JSON.stringify(voteData));
133
+ });
134
+
135
+ it("getAdminView sends GET to admin path", async () => {
136
+ let capturedUrl = "";
137
+
138
+ globalThis.fetch = async (input) => {
139
+ capturedUrl = String(input);
140
+ return new Response(
141
+ JSON.stringify({
142
+ data: { id: "poll_1", title: "Test" },
143
+ meta: { requestId: "r1" },
144
+ }),
145
+ { status: 200 },
146
+ );
147
+ };
148
+
149
+ const client = new MeetLarkClient("https://api.test");
150
+ await client.getAdminView("adm_abc123");
151
+
152
+ assert.strictEqual(
153
+ capturedUrl,
154
+ "https://api.test/api/v1/polls/admin/adm_abc123",
155
+ );
156
+ });
157
+
158
+ it("closePoll sends POST to close path", async () => {
159
+ let capturedUrl = "";
160
+ let capturedInit: RequestInit | undefined;
161
+
162
+ globalThis.fetch = async (input, init) => {
163
+ capturedUrl = String(input);
164
+ capturedInit = init;
165
+ return new Response(
166
+ JSON.stringify({ data: { success: true }, meta: { requestId: "r1" } }),
167
+ { status: 200 },
168
+ );
169
+ };
170
+
171
+ const client = new MeetLarkClient("https://api.test");
172
+ await client.closePoll("adm_abc123");
173
+
174
+ assert.strictEqual(
175
+ capturedUrl,
176
+ "https://api.test/api/v1/polls/admin/adm_abc123/close",
177
+ );
178
+ assert.strictEqual(capturedInit?.method, "POST");
179
+ });
180
+
181
+ it("reopenPoll sends POST to reopen path", async () => {
182
+ let capturedUrl = "";
183
+ let capturedInit: RequestInit | undefined;
184
+
185
+ globalThis.fetch = async (input, init) => {
186
+ capturedUrl = String(input);
187
+ capturedInit = init;
188
+ return new Response(
189
+ JSON.stringify({ data: { success: true }, meta: { requestId: "r1" } }),
190
+ { status: 200 },
191
+ );
192
+ };
193
+
194
+ const client = new MeetLarkClient("https://api.test");
195
+ await client.reopenPoll("adm_abc123");
196
+
197
+ assert.strictEqual(
198
+ capturedUrl,
199
+ "https://api.test/api/v1/polls/admin/adm_abc123/reopen",
200
+ );
201
+ assert.strictEqual(capturedInit?.method, "POST");
202
+ });
203
+
204
+ it("handles network error gracefully", async () => {
205
+ globalThis.fetch = async () => {
206
+ throw new Error("Connection refused");
207
+ };
208
+
209
+ const client = new MeetLarkClient("https://api.test");
210
+ const result = await client.createPoll({ title: "Test" });
211
+
212
+ assert.strictEqual(result.error?.status, 0);
213
+ assert.strictEqual(result.error?.code, "network_error");
214
+ assert.strictEqual(result.error?.message, "Connection refused");
215
+ assert.strictEqual(result.data, undefined);
216
+ });
217
+
218
+ it("uses custom API URL from config", async () => {
219
+ let capturedUrl = "";
220
+
221
+ globalThis.fetch = async (input) => {
222
+ capturedUrl = String(input);
223
+ return new Response(
224
+ JSON.stringify({ data: { id: "poll_1" }, meta: { requestId: "r1" } }),
225
+ { status: 201 },
226
+ );
227
+ };
228
+
229
+ const client = new MeetLarkClient("https://custom.example.com");
230
+ await client.createPoll({ title: "Test" });
231
+
232
+ assert.ok(capturedUrl.startsWith("https://custom.example.com"));
233
+ });
234
+ });
@@ -0,0 +1,112 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
4
+ import { createMeetLarkServer } from "../src/server.ts";
5
+
6
+ let originalFetch: typeof globalThis.fetch;
7
+
8
+ beforeEach(() => {
9
+ originalFetch = globalThis.fetch;
10
+ globalThis.fetch = async () =>
11
+ new Response(JSON.stringify({ data: {} }), { status: 200 });
12
+ });
13
+
14
+ afterEach(() => {
15
+ globalThis.fetch = originalFetch;
16
+ });
17
+
18
+ const MCP_HEADERS = {
19
+ "Content-Type": "application/json",
20
+ Accept: "application/json, text/event-stream",
21
+ };
22
+
23
+ /** Creates a fresh server + transport per request, matching the stateless route handler pattern. */
24
+ async function handleRequest(req: Request): Promise<Response> {
25
+ const server = createMeetLarkServer("https://api.test");
26
+ const transport = new WebStandardStreamableHTTPServerTransport({
27
+ sessionIdGenerator: undefined,
28
+ });
29
+ await server.connect(transport);
30
+ return transport.handleRequest(req);
31
+ }
32
+
33
+ /** Parse response body, handling both SSE and JSON content types. */
34
+ async function parseResponse(
35
+ res: Response,
36
+ ): Promise<Record<string, unknown>> {
37
+ const text = await res.text();
38
+ const contentType = res.headers.get("content-type") || "";
39
+
40
+ if (contentType.includes("text/event-stream")) {
41
+ const dataLine = text
42
+ .split("\n")
43
+ .find((line) => line.startsWith("data: "));
44
+ assert.ok(dataLine, "Expected data line in SSE response");
45
+ return JSON.parse(dataLine!.slice(6));
46
+ }
47
+
48
+ return JSON.parse(text);
49
+ }
50
+
51
+ describe("HTTP route handler", () => {
52
+ it("responds to MCP initialize request", async () => {
53
+ const res = await handleRequest(
54
+ new Request("http://localhost/mcp", {
55
+ method: "POST",
56
+ headers: MCP_HEADERS,
57
+ body: JSON.stringify({
58
+ jsonrpc: "2.0",
59
+ id: 1,
60
+ method: "initialize",
61
+ params: {
62
+ protocolVersion: "2025-03-26",
63
+ capabilities: {},
64
+ clientInfo: { name: "test-client", version: "1.0.0" },
65
+ },
66
+ }),
67
+ }),
68
+ );
69
+
70
+ assert.ok(res.status === 200, `Expected 200, got ${res.status}`);
71
+ const body = await parseResponse(res);
72
+ assert.strictEqual(body.jsonrpc, "2.0");
73
+ assert.strictEqual(body.id, 1);
74
+ assert.ok(body.result, "Expected result in response");
75
+ const result = body.result as Record<string, unknown>;
76
+ assert.ok(result.serverInfo, "Expected serverInfo");
77
+ const serverInfo = result.serverInfo as Record<string, unknown>;
78
+ assert.strictEqual(serverInfo.name, "meetlark");
79
+ });
80
+
81
+ it("returns tool list on tools/list request", async () => {
82
+ const res = await handleRequest(
83
+ new Request("http://localhost/mcp", {
84
+ method: "POST",
85
+ headers: MCP_HEADERS,
86
+ body: JSON.stringify({
87
+ jsonrpc: "2.0",
88
+ id: 1,
89
+ method: "tools/list",
90
+ params: {},
91
+ }),
92
+ }),
93
+ );
94
+
95
+ assert.ok(res.status === 200, `Expected 200, got ${res.status}`);
96
+ const body = await parseResponse(res);
97
+ assert.strictEqual(body.jsonrpc, "2.0");
98
+ assert.strictEqual(body.id, 1);
99
+ const result = body.result as { tools: { name: string }[] };
100
+ assert.ok(result.tools, "Expected tools in result");
101
+
102
+ const toolNames = result.tools.map((t) => t.name).sort();
103
+ assert.deepStrictEqual(toolNames, [
104
+ "meetlark_close_poll",
105
+ "meetlark_create_poll",
106
+ "meetlark_get_admin_view",
107
+ "meetlark_get_results",
108
+ "meetlark_reopen_poll",
109
+ "meetlark_vote",
110
+ ]);
111
+ });
112
+ });
@@ -0,0 +1,87 @@
1
+ import { describe, it, beforeEach, afterEach } from "node:test";
2
+ import assert from "node:assert";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
+ import { createMeetLarkServer } from "../src/server.ts";
7
+
8
+ let originalFetch: typeof globalThis.fetch;
9
+ let client: Client;
10
+
11
+ beforeEach(() => {
12
+ originalFetch = globalThis.fetch;
13
+ // Stub fetch so tool registration doesn't hit real network
14
+ globalThis.fetch = async () => new Response(JSON.stringify({ data: {} }), { status: 200 });
15
+ });
16
+
17
+ afterEach(async () => {
18
+ globalThis.fetch = originalFetch;
19
+ await client?.close();
20
+ });
21
+
22
+ describe("createMeetLarkServer", () => {
23
+ it("returns an McpServer instance", () => {
24
+ const server = createMeetLarkServer();
25
+ assert.ok(server instanceof McpServer);
26
+ });
27
+
28
+ it("has all 6 tools registered", async () => {
29
+ const server = createMeetLarkServer("https://api.test");
30
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
31
+ client = new Client({ name: "test-client", version: "1.0.0" });
32
+ await Promise.all([
33
+ client.connect(clientTransport),
34
+ server.connect(serverTransport),
35
+ ]);
36
+
37
+ const { tools } = await client.listTools();
38
+ const toolNames = tools.map((t) => t.name).sort();
39
+
40
+ assert.deepStrictEqual(toolNames, [
41
+ "meetlark_close_poll",
42
+ "meetlark_create_poll",
43
+ "meetlark_get_admin_view",
44
+ "meetlark_get_results",
45
+ "meetlark_reopen_poll",
46
+ "meetlark_vote",
47
+ ]);
48
+ });
49
+
50
+ it("uses default API URL when none provided", async () => {
51
+ const server = createMeetLarkServer();
52
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
53
+ client = new Client({ name: "test-client", version: "1.0.0" });
54
+ await Promise.all([
55
+ client.connect(clientTransport),
56
+ server.connect(serverTransport),
57
+ ]);
58
+
59
+ let capturedUrl = "";
60
+ globalThis.fetch = async (input) => {
61
+ capturedUrl = String(input);
62
+ return new Response(JSON.stringify({ data: { poll: {} }, meta: { requestId: "r1" } }), { status: 200 });
63
+ };
64
+
65
+ await client.callTool({ name: "meetlark_get_results", arguments: { participateToken: "prt_test" } });
66
+ assert.ok(capturedUrl.startsWith("https://meetlark.ai/"), `Expected default URL, got: ${capturedUrl}`);
67
+ });
68
+
69
+ it("uses custom API URL when provided", async () => {
70
+ const server = createMeetLarkServer("https://custom.example.com");
71
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
72
+ client = new Client({ name: "test-client", version: "1.0.0" });
73
+ await Promise.all([
74
+ client.connect(clientTransport),
75
+ server.connect(serverTransport),
76
+ ]);
77
+
78
+ let capturedUrl = "";
79
+ globalThis.fetch = async (input) => {
80
+ capturedUrl = String(input);
81
+ return new Response(JSON.stringify({ data: { poll: {} }, meta: { requestId: "r1" } }), { status: 200 });
82
+ };
83
+
84
+ await client.callTool({ name: "meetlark_get_results", arguments: { participateToken: "prt_test" } });
85
+ assert.ok(capturedUrl.startsWith("https://custom.example.com/"), `Expected custom URL, got: ${capturedUrl}`);
86
+ });
87
+ });
@@ -0,0 +1,27 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+
4
+ describe("config", () => {
5
+ it("exports MEETLARK_API_URL default", async () => {
6
+ const { MEETLARK_API_URL } = await import("../src/config.ts");
7
+ assert.strictEqual(MEETLARK_API_URL, "https://meetlark.ai");
8
+ });
9
+
10
+ it("respects MEETLARK_API_URL env var", async () => {
11
+ const original = process.env.MEETLARK_API_URL;
12
+ process.env.MEETLARK_API_URL = "https://custom.example.com";
13
+ try {
14
+ // Dynamic re-import won't re-evaluate the module in the same process,
15
+ // so we test the env var mechanism directly
16
+ const url =
17
+ process.env.MEETLARK_API_URL || "https://meetlark.ai";
18
+ assert.strictEqual(url, "https://custom.example.com");
19
+ } finally {
20
+ if (original === undefined) {
21
+ delete process.env.MEETLARK_API_URL;
22
+ } else {
23
+ process.env.MEETLARK_API_URL = original;
24
+ }
25
+ }
26
+ });
27
+ });