agent-social-mcp 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 ADDED
@@ -0,0 +1,29 @@
1
+ # Agent Social MCP Client
2
+
3
+ Agent Social installs anonymous social discovery, realtime conversation, and Agent-assisted reply analysis into compatible Agent hosts.
4
+
5
+ Node.js 22 or newer is required. For Codex, add:
6
+
7
+ ```toml
8
+ [mcp_servers.agent_social]
9
+ command = "npx"
10
+ args = ["-y", "agent-social-mcp@0.2.0"]
11
+
12
+ [mcp_servers.agent_social.env]
13
+ AGENT_SOCIAL_RELAY_URL = "https://agent-social.shenglongai.top"
14
+ ```
15
+
16
+ Restart Codex, then ask the Agent to confirm that `dating_onboard`, `dating_request_match`, and `dating_open_chat` are available. Installation and version checks must not onboard, consume an invite, create a match, or send a message.
17
+
18
+ The localhost fallback is:
19
+
20
+ ```bash
21
+ AGENT_SOCIAL_RELAY_URL=https://agent-social.shenglongai.top \
22
+ npx -y -p agent-social-mcp@0.2.0 agent-social-chat
23
+ ```
24
+
25
+ Open `http://127.0.0.1:8787` on that computer. The default owner-only identity and chat state remain under `$HOME/.agent-social-mcp`; an existing `AGENT_SOCIAL_STORE_PATH` remains compatible.
26
+
27
+ This public package contains the local client only. It does not contain the central relay, database adapters, deployment code, test harness, operator keys, invites, user credentials, or production data.
28
+
29
+ See `USAGE.md` for the preview usage status and safety boundaries.
package/USAGE.md ADDED
@@ -0,0 +1,7 @@
1
+ # Preview Usage Notice
2
+
3
+ This `0.2.0` package is an `UNLICENSED` public preview. Public registry availability permits installation and evaluation of the Agent Social client but is not an open-source license grant.
4
+
5
+ Do not redistribute credentials, copy another user's local state, bypass invite onboarding, automate social message sending, or use the client to access a relay without authorization.
6
+
7
+ The client does not verify identity, age, gender, devices, photos, or real-world safety. Users control outbound messages and should use report, block, and close controls when appropriate.
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { createRemoteDatingMcpServer } from "./remoteMcpServer.js";
6
+ const relayUrl = process.env.AGENT_SOCIAL_RELAY_URL?.trim();
7
+ if (!relayUrl) {
8
+ throw new Error("AGENT_SOCIAL_RELAY_URL is required for the public Agent Social client");
9
+ }
10
+ const storePath = process.env.AGENT_SOCIAL_STORE_PATH
11
+ ?? join(homedir(), ".agent-social-mcp", "state.json");
12
+ const server = createRemoteDatingMcpServer({ relayUrl, storePath });
13
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,20 @@
1
+ const actorFields = {
2
+ dating_onboard: "existingToken",
3
+ dating_request_match: "userToken",
4
+ dating_get_match_status: "userToken",
5
+ dating_send_message: "senderToken",
6
+ dating_receive_messages: "recipientToken",
7
+ dating_mark_read: "readerToken",
8
+ dating_report_session: "reporterToken",
9
+ dating_block_user: "blockerToken",
10
+ dating_close_session: "requesterToken"
11
+ };
12
+ export function actorTokenForClientCall(tool, input) {
13
+ const field = actorFields[tool];
14
+ const value = field ? input[field] : undefined;
15
+ return typeof value === "string" && value.trim() ? value : undefined;
16
+ }
17
+ export function authorizationForClientRelayCall(tool, input) {
18
+ const token = actorTokenForClientCall(tool, input);
19
+ return token ? `Bearer ${token}` : undefined;
20
+ }
@@ -0,0 +1,142 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { registerCompanionMcpUi } from "../companion/mcpUi.js";
4
+ import { syncCompanionState } from "../companion/syncState.js";
5
+ import { CompanionStateStore, defaultCompanionStatePath } from "../companion/state.js";
6
+ import { MCP_TOOLS } from "../contracts.js";
7
+ import { authorizationForClientRelayCall } from "./relayAuth.js";
8
+ const optionalDateString = z.string().datetime().optional();
9
+ const datingIntentSchema = z.enum(["light_chat", "flirt", "relationship"]);
10
+ const genderPreferenceSchema = z.enum(["men", "women", "any"]);
11
+ export function createRemoteDatingMcpServer(options) {
12
+ const relayUrl = normalizedRelayUrl(options.relayUrl);
13
+ const server = new McpServer({
14
+ name: "agent-social-mcp",
15
+ version: "0.2.0"
16
+ });
17
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.onboard, "Dating Onboard", "Create or reuse a persistent anonymous dating relay token.", {
18
+ existingToken: z.string().optional(),
19
+ inviteCode: z.string(),
20
+ declaredAdult: z.boolean(),
21
+ now: optionalDateString
22
+ });
23
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.requestMatch, "Dating Request Match", "Submit a temporary match capsule and try to create one compatible match session.", {
24
+ userToken: z.string(),
25
+ datingIntent: datingIntentSchema,
26
+ genderPreference: genderPreferenceSchema,
27
+ desiredTone: z.string(),
28
+ topics: z.array(z.string()).min(1),
29
+ boundaries: z.array(z.string()).default([]),
30
+ shareableIntro: z.string(),
31
+ ttlMinutes: z.number().int().min(1).max(1440),
32
+ createdAt: optionalDateString
33
+ });
34
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.getMatchStatus, "Dating Get Match Status", "Read active or historical match sessions for an authenticated anonymous user.", {
35
+ userToken: z.string()
36
+ });
37
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.sendMessage, "Dating Send Message", "Relay an outbound dating message only after explicit user confirmation.", {
38
+ id: z.string().optional(),
39
+ sessionId: z.string(),
40
+ senderToken: z.string(),
41
+ body: z.string(),
42
+ userConfirmed: z.boolean(),
43
+ createdAt: optionalDateString
44
+ });
45
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.receiveMessages, "Dating Receive Messages", "Fetch inbound messages for a session with structured reply context for the user's Agent.", {
46
+ sessionId: z.string(),
47
+ recipientToken: z.string(),
48
+ cursor: z.string().optional()
49
+ });
50
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.markRead, "Dating Mark Read", "Mark peer messages in an active session as read by the authenticated user.", {
51
+ sessionId: z.string(),
52
+ readerToken: z.string(),
53
+ readAt: optionalDateString
54
+ });
55
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.reportSession, "Dating Report Session", "Report a match session and retain available message context for safety review.", {
56
+ id: z.string().optional(),
57
+ sessionId: z.string(),
58
+ reporterToken: z.string(),
59
+ reason: z.string(),
60
+ createdAt: optionalDateString
61
+ });
62
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.blockUser, "Dating Block User", "Block the peer in a match session without exposing their credential.", {
63
+ sessionId: z.string(),
64
+ blockerToken: z.string(),
65
+ createdAt: optionalDateString
66
+ });
67
+ registerRemoteTool(server, options, relayUrl, MCP_TOOLS.closeSession, "Dating Close Session", "Close an active match session for one of its participants.", {
68
+ sessionId: z.string(),
69
+ requesterToken: z.string()
70
+ });
71
+ const stateStore = new CompanionStateStore(defaultCompanionStatePath({
72
+ ...process.env,
73
+ AGENT_SOCIAL_STORE_PATH: options.storePath
74
+ }));
75
+ registerCompanionMcpUi(server, {
76
+ stateStore,
77
+ relay: {
78
+ call: (tool, input) => postRemoteRelayTool(options, relayUrl, tool, input)
79
+ }
80
+ });
81
+ return server;
82
+ }
83
+ function registerRemoteTool(server, options, relayUrl, name, title, description, inputSchema) {
84
+ const registerTool = server.registerTool.bind(server);
85
+ registerTool(name, { title, description, inputSchema }, async (input) => {
86
+ const rawInput = input;
87
+ const payload = await postRemoteRelayTool(options, relayUrl, name, rawInput);
88
+ if (payload.ok !== false) {
89
+ syncCompanionState(options.storePath, name, payload, rawInput);
90
+ }
91
+ return payload.ok === false
92
+ ? toolError(String(payload.error ?? "relay error"), payload)
93
+ : toolSuccess(payload);
94
+ });
95
+ }
96
+ async function postRemoteRelayTool(options, relayUrl, tool, input) {
97
+ const authorization = authorizationForClientRelayCall(tool, input);
98
+ try {
99
+ const response = await (options.fetchImpl ?? fetch)(new URL("/api/relay", relayUrl), {
100
+ method: "POST",
101
+ headers: {
102
+ "content-type": "application/json",
103
+ ...(authorization ? { authorization } : {})
104
+ },
105
+ body: JSON.stringify({ tool, input })
106
+ });
107
+ const payload = (await response.json());
108
+ if (!response.ok) {
109
+ return {
110
+ ok: false,
111
+ error: typeof payload.error === "string" ? payload.error : `relay http ${response.status}`
112
+ };
113
+ }
114
+ return payload;
115
+ }
116
+ catch {
117
+ return { ok: false, error: "relay request failed" };
118
+ }
119
+ }
120
+ function normalizedRelayUrl(value) {
121
+ if (!value.trim()) {
122
+ throw new Error("AGENT_SOCIAL_RELAY_URL is required");
123
+ }
124
+ const url = new URL(value);
125
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
126
+ throw new Error("AGENT_SOCIAL_RELAY_URL must use http or https");
127
+ }
128
+ return url;
129
+ }
130
+ function toolSuccess(payload) {
131
+ return {
132
+ content: [{ type: "text", text: "OK" }],
133
+ structuredContent: JSON.parse(JSON.stringify(payload))
134
+ };
135
+ }
136
+ function toolError(message, payload) {
137
+ return {
138
+ content: [{ type: "text", text: message }],
139
+ structuredContent: JSON.parse(JSON.stringify(payload)),
140
+ isError: true
141
+ };
142
+ }
@@ -0,0 +1,369 @@
1
+ import { createServer } from "node:http";
2
+ import { MCP_TOOLS } from "../contracts.js";
3
+ import { RelayClient } from "./relayClient.js";
4
+ import { buildReplyAnalysisContext, shouldAutoAnalyzeReply } from "./replyAssistance.js";
5
+ import { chatHtml as sharedChatHtml } from "./chatUi.js";
6
+ export function createChatServer(options) {
7
+ const events = new CompanionEventHub(options);
8
+ return createServer(async (request, response) => {
9
+ if (request.method === "GET" && new URL(request.url ?? "/", "http://localhost").pathname === "/api/events") {
10
+ events.attach(request, response);
11
+ return;
12
+ }
13
+ try {
14
+ const result = await handleChatHttpRequest(options, {
15
+ method: request.method,
16
+ url: request.url,
17
+ body: await readRawBody(request)
18
+ });
19
+ writeResponse(response, result);
20
+ if (request.method === "POST" && result.statusCode < 400) {
21
+ events.publishState();
22
+ }
23
+ }
24
+ catch (error) {
25
+ writeResponse(response, jsonResponse(500, {
26
+ ok: false,
27
+ error: error instanceof Error ? error.message : "chat internal error"
28
+ }));
29
+ }
30
+ });
31
+ }
32
+ class CompanionEventHub {
33
+ options;
34
+ clients = new Set();
35
+ timer;
36
+ syncing = false;
37
+ retryMs = 1000;
38
+ constructor(options) {
39
+ this.options = options;
40
+ }
41
+ attach(request, response) {
42
+ response.writeHead(200, {
43
+ "content-type": "text/event-stream; charset=utf-8",
44
+ "cache-control": "no-cache, no-transform",
45
+ connection: "keep-alive"
46
+ });
47
+ response.write("retry: 1000\n\n");
48
+ this.clients.add(response);
49
+ this.publishState();
50
+ this.schedule(0);
51
+ request.on("close", () => {
52
+ this.clients.delete(response);
53
+ if (this.clients.size === 0 && this.timer) {
54
+ clearTimeout(this.timer);
55
+ this.timer = undefined;
56
+ }
57
+ });
58
+ }
59
+ publishState() {
60
+ this.broadcast("state", summarizeState(this.options.stateStore.read()));
61
+ }
62
+ schedule(delay) {
63
+ if (this.clients.size === 0 || this.timer) {
64
+ return;
65
+ }
66
+ this.timer = setTimeout(() => {
67
+ this.timer = undefined;
68
+ void this.tick();
69
+ }, delay);
70
+ this.timer.unref();
71
+ }
72
+ async tick() {
73
+ if (this.syncing || this.clients.size === 0) {
74
+ this.schedule(this.retryMs);
75
+ return;
76
+ }
77
+ this.syncing = true;
78
+ try {
79
+ const relay = new RelayClient({ relayUrl: this.options.relayUrl });
80
+ await syncMessages(this.options.stateStore, relay);
81
+ this.retryMs = 1000;
82
+ this.broadcast("status", { online: true, syncedAt: new Date().toISOString() });
83
+ this.publishState();
84
+ }
85
+ catch (error) {
86
+ this.retryMs = Math.min(this.retryMs * 2, 15000);
87
+ this.broadcast("status", {
88
+ online: false,
89
+ error: error instanceof Error ? error.message : "sync failed",
90
+ retryMs: this.retryMs
91
+ });
92
+ }
93
+ finally {
94
+ this.syncing = false;
95
+ this.schedule(this.retryMs);
96
+ }
97
+ }
98
+ broadcast(event, payload) {
99
+ const frame = `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
100
+ for (const client of this.clients) {
101
+ client.write(frame);
102
+ }
103
+ }
104
+ }
105
+ export async function handleChatHttpRequest(options, request) {
106
+ const url = new URL(request.url ?? "/", "http://localhost");
107
+ const method = request.method ?? "GET";
108
+ const relay = new RelayClient({ relayUrl: options.relayUrl });
109
+ if (method === "GET" && url.pathname === "/") {
110
+ return htmlResponse(sharedChatHtml);
111
+ }
112
+ if (method === "GET" && url.pathname === "/api/state") {
113
+ return jsonResponse(200, summarizeState(options.stateStore.read()));
114
+ }
115
+ if (method === "POST" && url.pathname === "/api/state") {
116
+ const body = parseJsonBody(request.body);
117
+ const userToken = stringValue(body.userToken);
118
+ if (!userToken) {
119
+ return jsonResponse(400, { ok: false, error: "userToken is required" });
120
+ }
121
+ return jsonResponse(200, summarizeState(options.stateStore.setUserToken(userToken)));
122
+ }
123
+ if (method === "POST" && url.pathname === "/api/sessions/sync") {
124
+ const state = await syncSessions(options.stateStore, relay);
125
+ return jsonResponse(200, summarizeState(state));
126
+ }
127
+ if (method === "POST" && url.pathname === "/api/messages/sync") {
128
+ const body = parseJsonBody(request.body);
129
+ const state = await syncMessages(options.stateStore, relay, stringOrUndefined(body.sessionId));
130
+ return jsonResponse(200, summarizeState(state));
131
+ }
132
+ if (method === "POST" && url.pathname === "/api/messages/send") {
133
+ const body = parseJsonBody(request.body);
134
+ const result = await sendMessage(options.stateStore, relay, body);
135
+ return jsonResponse(200, result);
136
+ }
137
+ if (method === "POST" && url.pathname === "/api/preferences") {
138
+ const body = parseJsonBody(request.body);
139
+ if (typeof body.autoAnalyze !== "boolean") {
140
+ return jsonResponse(400, { ok: false, error: "autoAnalyze must be boolean" });
141
+ }
142
+ return jsonResponse(200, summarizeState(options.stateStore.setAutoAnalyze(body.autoAnalyze)));
143
+ }
144
+ if (method === "POST" && url.pathname === "/api/sessions/read") {
145
+ const body = parseJsonBody(request.body);
146
+ const sessionId = stringValue(body.sessionId);
147
+ if (!sessionId) {
148
+ return jsonResponse(400, { ok: false, error: "sessionId is required" });
149
+ }
150
+ return jsonResponse(200, await markSessionRead(options.stateStore, relay, sessionId));
151
+ }
152
+ if (method === "POST" && url.pathname === "/api/sessions/report") {
153
+ const body = parseJsonBody(request.body);
154
+ return jsonResponse(200, await runSessionAction(options.stateStore, relay, "report", body));
155
+ }
156
+ if (method === "POST" && url.pathname === "/api/sessions/block") {
157
+ const body = parseJsonBody(request.body);
158
+ return jsonResponse(200, await runSessionAction(options.stateStore, relay, "block", body));
159
+ }
160
+ if (method === "POST" && url.pathname === "/api/sessions/close") {
161
+ const body = parseJsonBody(request.body);
162
+ return jsonResponse(200, await runSessionAction(options.stateStore, relay, "close", body));
163
+ }
164
+ return jsonResponse(404, { ok: false, error: "not found" });
165
+ }
166
+ export async function syncSessions(stateStore, relay) {
167
+ const state = stateStore.read();
168
+ if (!state.userToken) {
169
+ return state;
170
+ }
171
+ const payload = await relay.call(MCP_TOOLS.getMatchStatus, { userToken: state.userToken });
172
+ if (payload.ok === false) {
173
+ throw new Error(String(payload.error ?? "failed to sync sessions"));
174
+ }
175
+ return stateStore.upsertSessions((payload.sessions ?? []));
176
+ }
177
+ export async function syncMessages(stateStore, relay, sessionId) {
178
+ let state = await syncSessions(stateStore, relay);
179
+ if (!state.userToken) {
180
+ return state;
181
+ }
182
+ const sessions = state.sessions.filter((session) => session.status === "active" && (!sessionId || session.id === sessionId));
183
+ for (const session of sessions) {
184
+ const payload = await relay.call(MCP_TOOLS.receiveMessages, {
185
+ sessionId: session.id,
186
+ recipientToken: state.userToken,
187
+ ...(session.cursor ? { cursor: session.cursor } : {})
188
+ });
189
+ if (payload.ok === false) {
190
+ if (sessionId) {
191
+ throw new Error(String(payload.error ?? "failed to sync messages"));
192
+ }
193
+ continue;
194
+ }
195
+ const messages = (payload.messages ?? []);
196
+ state = stateStore.upsertMessages(messages);
197
+ state = stateStore.setSessionCursor(session.id, typeof payload.cursor === "string" ? payload.cursor : undefined);
198
+ }
199
+ return state;
200
+ }
201
+ export async function sendMessage(stateStore, relay, body) {
202
+ const state = stateStore.read();
203
+ if (!state.userToken) {
204
+ return { ok: false, error: "userToken is required" };
205
+ }
206
+ const sessionId = stringValue(body.sessionId);
207
+ const text = stringValue(body.body);
208
+ if (!sessionId) {
209
+ return { ok: false, error: "sessionId is required" };
210
+ }
211
+ if (!text.trim()) {
212
+ return { ok: false, error: "body is required" };
213
+ }
214
+ const payload = await relay.call(MCP_TOOLS.sendMessage, {
215
+ sessionId,
216
+ senderToken: state.userToken,
217
+ body: text,
218
+ userConfirmed: true
219
+ });
220
+ if (payload.ok === false) {
221
+ return payload;
222
+ }
223
+ stateStore.upsertMessages([payload.message], "sent");
224
+ return {
225
+ ok: true,
226
+ state: summarizeState(stateStore.read())
227
+ };
228
+ }
229
+ export function summarizeState(state) {
230
+ const analysisTargets = state.sessions.flatMap((session) => {
231
+ const context = buildReplyAnalysisContext({
232
+ sessionId: session.id,
233
+ peerIntro: session.peerIntro,
234
+ matchContext: state.matchContext,
235
+ messages: state.messages.filter((message) => message.sessionId === session.id)
236
+ });
237
+ if (!context) {
238
+ return [];
239
+ }
240
+ const assistance = state.replyAssistance.find((candidate) => candidate.sessionId === session.id);
241
+ const request = state.replyAssistanceRequests.find((candidate) => candidate.sessionId === session.id);
242
+ const status = request?.contextHash === context.contextHash
243
+ ? "analyzing"
244
+ : assistance?.contextHash === context.contextHash
245
+ ? "ready"
246
+ : assistance
247
+ ? "stale"
248
+ : "idle";
249
+ return [{
250
+ sessionId: session.id,
251
+ sourceMessageId: context.sourceMessageId,
252
+ contextHash: context.contextHash,
253
+ autoEligible: shouldAutoAnalyzeReply(context),
254
+ status,
255
+ ...(request ? { requestCreatedAt: request.createdAt } : {})
256
+ }];
257
+ });
258
+ return {
259
+ ok: true,
260
+ connected: Boolean(state.userToken),
261
+ preferences: state.preferences,
262
+ sessions: state.sessions.map((session) => {
263
+ const messages = state.messages.filter((message) => message.sessionId === session.id);
264
+ const lastMessage = messages.at(-1);
265
+ const unreadCount = messages.filter((message) => message.direction === "received" && !message.readAt).length;
266
+ return {
267
+ ...session,
268
+ createdAt: session.createdAt,
269
+ unreadCount,
270
+ lastMessageAt: lastMessage?.createdAt,
271
+ lastMessagePreview: lastMessage?.body
272
+ };
273
+ }),
274
+ messages: state.messages,
275
+ analysisTargets,
276
+ replyAssistance: state.replyAssistance
277
+ };
278
+ }
279
+ export async function markSessionRead(stateStore, relay, sessionId) {
280
+ const state = stateStore.read();
281
+ if (!state.userToken) {
282
+ return { ok: false, error: "userToken is required" };
283
+ }
284
+ const payload = await relay.call(MCP_TOOLS.markRead, {
285
+ sessionId,
286
+ readerToken: state.userToken,
287
+ readAt: new Date().toISOString()
288
+ });
289
+ if (payload.ok === false) {
290
+ return payload;
291
+ }
292
+ stateStore.markSessionOpened(sessionId);
293
+ await syncMessages(stateStore, relay, sessionId);
294
+ return summarizeState(stateStore.read());
295
+ }
296
+ export async function runSessionAction(stateStore, relay, action, body) {
297
+ const state = stateStore.read();
298
+ const sessionId = stringValue(body.sessionId);
299
+ if (!state.userToken) {
300
+ return { ok: false, error: "userToken is required" };
301
+ }
302
+ if (!sessionId) {
303
+ return { ok: false, error: "sessionId is required" };
304
+ }
305
+ const calls = {
306
+ report: {
307
+ tool: MCP_TOOLS.reportSession,
308
+ input: {
309
+ sessionId,
310
+ reporterToken: state.userToken,
311
+ reason: stringValue(body.reason) || "User reported this session from the live chat UI.",
312
+ createdAt: new Date().toISOString()
313
+ }
314
+ },
315
+ block: {
316
+ tool: MCP_TOOLS.blockUser,
317
+ input: { sessionId, blockerToken: state.userToken, createdAt: new Date().toISOString() }
318
+ },
319
+ close: {
320
+ tool: MCP_TOOLS.closeSession,
321
+ input: { sessionId, requesterToken: state.userToken }
322
+ }
323
+ };
324
+ const call = calls[action];
325
+ const payload = await relay.call(call.tool, call.input);
326
+ if (payload.ok === false) {
327
+ return payload;
328
+ }
329
+ await syncSessions(stateStore, relay);
330
+ return { ...summarizeState(stateStore.read()), action };
331
+ }
332
+ async function readRawBody(request) {
333
+ const chunks = [];
334
+ for await (const chunk of request) {
335
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
336
+ }
337
+ return Buffer.concat(chunks);
338
+ }
339
+ function parseJsonBody(body) {
340
+ if (!body) {
341
+ return {};
342
+ }
343
+ const text = Buffer.isBuffer(body) ? body.toString("utf8") : body;
344
+ return text ? JSON.parse(text) : {};
345
+ }
346
+ function stringValue(value) {
347
+ return typeof value === "string" ? value : "";
348
+ }
349
+ function stringOrUndefined(value) {
350
+ return typeof value === "string" && value.trim() ? value : undefined;
351
+ }
352
+ function jsonResponse(statusCode, body) {
353
+ return {
354
+ statusCode,
355
+ headers: { "content-type": "application/json; charset=utf-8" },
356
+ body: JSON.stringify(body)
357
+ };
358
+ }
359
+ function htmlResponse(body) {
360
+ return {
361
+ statusCode: 200,
362
+ headers: { "content-type": "text/html; charset=utf-8" },
363
+ body
364
+ };
365
+ }
366
+ function writeResponse(response, result) {
367
+ response.writeHead(result.statusCode, result.headers ?? {});
368
+ response.end(result.body);
369
+ }