sqlitedata-swift-mcp 1.1.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,61 @@
1
+ # SQLiteData Swift MCP Server
2
+
3
+ Model Context Protocol server for the SQLiteData skills collection.
4
+
5
+ Exposes SQLiteData skills as MCP resources, commands as prompts, and tools for ask-style routing, search, and skill reads.
6
+
7
+ ## Install
8
+
9
+ ### Published package
10
+
11
+ ```bash
12
+ npx -y sqlitedata-swift-mcp
13
+ ```
14
+
15
+ ### From the repo
16
+
17
+ ```bash
18
+ node mcp-server/src/server.mjs
19
+ ```
20
+
21
+ ## Example MCP Config
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "sqlitedata-swift": {
27
+ "command": "npx",
28
+ "args": ["-y", "sqlitedata-swift-mcp"]
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### Local checkout
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "sqlitedata-swift": {
40
+ "command": "node",
41
+ "args": ["/path/to/sqlite-data/mcp-server/src/server.mjs"]
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ## Tools
48
+
49
+ - `ask` — route a question to the best skill
50
+ - `list_skills` — enumerate all skills
51
+ - `search_skills` — search by name/alias/description
52
+ - `get_skill` — retrieve a specific skill
53
+
54
+ ## Resources
55
+
56
+ - `sqlitedata-swift://skills/{name}` — one resource per skill
57
+
58
+ ## Prompts
59
+
60
+ - `ask` — with skill routing
61
+ - `audit` — with anti-pattern checklist
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../src/server.mjs";
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "sqlitedata-swift-mcp",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "MCP server for SQLiteData Swift skills \u2014 @Table models, CloudKit sync, API reference, and diagnostics",
6
+ "bin": {
7
+ "sqlitedata-swift-mcp": "./bin/sqlitedata-swift-mcp.mjs"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "src/",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "license": "MIT"
18
+ }
@@ -0,0 +1,303 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ export const DEFAULT_SKILLS_ROOT = path.resolve(__dirname, "../../skills");
7
+ export const DEFAULT_COMMANDS_ROOT = path.resolve(__dirname, "../../commands");
8
+
9
+ function toPosixPath(value) {
10
+ return value.split(path.sep).join("/");
11
+ }
12
+
13
+ function loadFrontmatter(markdown) {
14
+ const match = markdown.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
15
+ if (!match) return { attributes: {}, body: markdown };
16
+
17
+ const attributes = {};
18
+ for (const rawLine of match[1].split(/\r?\n/)) {
19
+ const line = rawLine.trim();
20
+ if (!line || line.startsWith("#") || !line.includes(":")) continue;
21
+ const sep = line.indexOf(":");
22
+ const key = line.slice(0, sep).trim();
23
+ let value = line.slice(sep + 1).trim();
24
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
25
+ value = value.slice(1, -1);
26
+ }
27
+ attributes[key] = value;
28
+ }
29
+
30
+ return { attributes, body: markdown.slice(match[0].length) };
31
+ }
32
+
33
+ function extractTitle(markdown, fallback) {
34
+ const match = markdown.match(/^#\s+(.+)$/m);
35
+ return match?.[1]?.replaceAll("`", "").trim() || fallback;
36
+ }
37
+
38
+ function wordTokens(query) {
39
+ return String(query ?? "").toLowerCase().split(/[^a-z0-9_]+/i).filter(Boolean);
40
+ }
41
+
42
+ function makeSnippet(markdown, query) {
43
+ const singleLine = markdown.replace(/\s+/g, " ").trim();
44
+ if (!singleLine) return "";
45
+ const lower = singleLine.toLowerCase();
46
+ const normalized = String(query ?? "").trim().toLowerCase();
47
+ const hit = normalized ? lower.indexOf(normalized) : -1;
48
+ const start = hit >= 0 ? Math.max(0, hit - 80) : 0;
49
+ const snippet = singleLine.slice(start, start + 220).trim();
50
+ return start > 0 ? `...${snippet}` : snippet;
51
+ }
52
+
53
+ function scoreSkill(skill, query, tokens) {
54
+ const lowerQuery = query.toLowerCase();
55
+ const haystacks = [
56
+ skill.name.toLowerCase(),
57
+ skill.title.toLowerCase(),
58
+ skill.description.toLowerCase(),
59
+ skill.markdown.toLowerCase(),
60
+ ...skill.aliases.map((a) => a.toLowerCase()),
61
+ ];
62
+
63
+ let score = 0;
64
+ for (const haystack of haystacks) {
65
+ if (haystack.includes(lowerQuery)) {
66
+ score += haystack === skill.markdown.toLowerCase() ? 20 : 80;
67
+ }
68
+ }
69
+ for (const token of tokens) {
70
+ for (const haystack of haystacks) {
71
+ if (haystack.includes(token)) {
72
+ score += haystack === skill.markdown.toLowerCase() ? 2 : 12;
73
+ }
74
+ }
75
+ }
76
+ return score;
77
+ }
78
+
79
+ function routePatterns() {
80
+ // Order matters: more specific patterns first, broad patterns last
81
+ return [
82
+ {
83
+ name: "sqlitedata-swift-diag",
84
+ reason: "matched error or troubleshooting terms",
85
+ patterns: [
86
+ /\b(error|debug|debugging|troubleshoot|fail|fails|failing|crash|constraint|permission|not working)\b/i,
87
+ /\bwhy does\b/i,
88
+ ],
89
+ },
90
+ {
91
+ name: "sqlitedata-swift-swiftdata-sync",
92
+ reason: "matched SwiftData comparison terms",
93
+ patterns: [/\b(SwiftData sync|SwiftData.*compar|NSPersistentCloudKitContainer|ModelConfiguration)\b/i],
94
+ },
95
+ {
96
+ name: "sqlitedata-swift-icloud-services",
97
+ reason: "matched iCloud setup terms",
98
+ patterns: [/\b(iCloud capability|entitlement|iCloud container|Xcode iCloud|iCloud.*setup|iCloud.*capability)\b/i],
99
+ },
100
+ {
101
+ name: "sqlitedata-swift-deploy-schema",
102
+ reason: "matched schema deployment terms",
103
+ patterns: [/\b(deploy schema|cloudkit console|production schema|reset development|schema.*production)\b/i],
104
+ },
105
+ {
106
+ name: "sqlitedata-swift-shared-records",
107
+ reason: "matched sharing terms",
108
+ patterns: [/\b(CKShare|shared records|sharing permissions|participants|UICloudSharingController)\b/i],
109
+ },
110
+ {
111
+ name: "sqlitedata-swift-ref",
112
+ reason: "matched API reference terms",
113
+ patterns: [
114
+ /\b(api signature|type signature|method signature|init parameter)\b/i,
115
+ /\bwhat methods\b/i,
116
+ /\b(FetchKeyRequest|DefaultDatabase|SyncMetadata)\b.*\b(signature|type|init|api)\b/i,
117
+ ],
118
+ },
119
+ {
120
+ name: "sqlitedata-swift-cloudkit",
121
+ reason: "matched CloudKit or sync terms",
122
+ patterns: [
123
+ /\b(SyncEngine|CloudKit sync|CKRecord|SyncMetadata)\b/i,
124
+ /\bcloudkit\b/i,
125
+ ],
126
+ },
127
+ {
128
+ name: "sqlitedata-swift-core",
129
+ reason: "matched core pattern terms",
130
+ patterns: [
131
+ /\b(@Table|@FetchAll|@FetchOne|@Fetch|FetchKeyRequest|@Selection|@Column|DatabaseMigrator|prepareDependencies|defaultDatabase)\b/i,
132
+ /\b(migration|insert|update|delete|join|leftJoin|@Observable|@ObservationIgnored)\b/i,
133
+ ],
134
+ },
135
+ ];
136
+ }
137
+
138
+ export function loadPluginCatalog(skillsRoot = DEFAULT_SKILLS_ROOT, commandsRoot = DEFAULT_COMMANDS_ROOT) {
139
+ const skillMetadataPath = path.join(skillsRoot, "catalog.json");
140
+ const skillMetadata = existsSync(skillMetadataPath)
141
+ ? JSON.parse(readFileSync(skillMetadataPath, "utf8")).skills ?? []
142
+ : [];
143
+ const metadataByName = new Map(skillMetadata.filter((e) => e?.name).map((e) => [e.name, e]));
144
+
145
+ const skills = [];
146
+ for (const entry of readdirSync(skillsRoot, { withFileTypes: true })) {
147
+ if (!entry.isDirectory()) continue;
148
+ const skillPath = path.join(skillsRoot, entry.name, "SKILL.md");
149
+ if (!existsSync(skillPath)) continue;
150
+
151
+ const markdown = readFileSync(skillPath, "utf8");
152
+ const { attributes, body } = loadFrontmatter(markdown);
153
+ const metadata = metadataByName.get(entry.name) ?? {};
154
+ const name = attributes.name || entry.name;
155
+
156
+ skills.push({
157
+ name,
158
+ title: extractTitle(body, entry.name),
159
+ description: attributes.description || metadata.description || "",
160
+ category: metadata.category || null,
161
+ kind: metadata.kind || null,
162
+ entrypointPriority: metadata.entrypoint_priority ?? Number.MAX_SAFE_INTEGER,
163
+ aliases: Array.isArray(metadata.aliases) ? metadata.aliases : [],
164
+ relatedSkills: Array.isArray(metadata.related_skills) ? metadata.related_skills : [],
165
+ uri: `sqlitedata-swift://skills/${encodeURIComponent(name)}`,
166
+ relativePath: toPosixPath(path.relative(skillsRoot, skillPath)),
167
+ markdown,
168
+ });
169
+ }
170
+ skills.sort((a, b) => a.name.localeCompare(b.name));
171
+
172
+ const commands = [];
173
+ if (existsSync(commandsRoot)) {
174
+ for (const entry of readdirSync(commandsRoot, { withFileTypes: true })) {
175
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
176
+ const commandPath = path.join(commandsRoot, entry.name);
177
+ const markdown = readFileSync(commandPath, "utf8");
178
+ const { attributes, body } = loadFrontmatter(markdown);
179
+ const name = path.basename(entry.name, ".md");
180
+
181
+ commands.push({
182
+ name,
183
+ title: extractTitle(body, name),
184
+ description: attributes.description || "",
185
+ argumentHint: attributes["argument-hint"] || "",
186
+ markdown,
187
+ uri: `sqlitedata-swift://commands/${encodeURIComponent(name)}`,
188
+ relativePath: toPosixPath(path.relative(commandsRoot, commandPath)),
189
+ });
190
+ }
191
+ }
192
+ commands.sort((a, b) => a.name.localeCompare(b.name));
193
+
194
+ return {
195
+ skills,
196
+ commands,
197
+ skillByName: new Map(skills.map((s) => [s.name.toLowerCase(), s])),
198
+ skillByUri: new Map(skills.map((s) => [s.uri, s])),
199
+ commandByName: new Map(commands.map((c) => [c.name.toLowerCase(), c])),
200
+ };
201
+ }
202
+
203
+ export function listSkills(catalog) {
204
+ return catalog.skills.map((s) => ({
205
+ name: s.name, title: s.title, description: s.description,
206
+ category: s.category, kind: s.kind, uri: s.uri, relatedSkills: s.relatedSkills,
207
+ }));
208
+ }
209
+
210
+ export function findSkill(catalog, locator = {}) {
211
+ if (locator.uri) return catalog.skillByUri.get(String(locator.uri)) ?? null;
212
+ if (locator.name) return catalog.skillByName.get(String(locator.name).toLowerCase()) ?? null;
213
+ return null;
214
+ }
215
+
216
+ export function searchSkills(catalog, query, limit = 5) {
217
+ const trimmed = String(query ?? "").trim();
218
+ if (!trimmed) return [];
219
+ const tokens = wordTokens(trimmed);
220
+ return catalog.skills
221
+ .map((s) => ({ skill: s, score: scoreSkill(s, trimmed, tokens) }))
222
+ .filter((e) => e.score > 0)
223
+ .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
224
+ .slice(0, limit)
225
+ .map(({ skill, score }) => ({
226
+ name: skill.name, title: skill.title, description: skill.description,
227
+ category: skill.category, kind: skill.kind, uri: skill.uri, score,
228
+ snippet: makeSnippet(skill.markdown, trimmed),
229
+ }));
230
+ }
231
+
232
+ export function routeAsk(catalog, question) {
233
+ const normalized = String(question ?? "").trim();
234
+ if (!normalized) return null;
235
+
236
+ for (const route of routePatterns()) {
237
+ if (route.patterns.some((p) => p.test(normalized))) {
238
+ const skill = findSkill(catalog, { name: route.name });
239
+ if (skill) return { skill, reason: route.reason };
240
+ }
241
+ }
242
+
243
+ const [best] = searchSkills(catalog, normalized, 1);
244
+ if (best) {
245
+ const skill = findSkill(catalog, { name: best.name });
246
+ if (skill) return { skill, reason: "matched the closest skill by aliases and description" };
247
+ }
248
+
249
+ const fallback = findSkill(catalog, { name: "sqlitedata-swift" });
250
+ return fallback ? { skill: fallback, reason: "fell back to the broad SQLiteData router" } : null;
251
+ }
252
+
253
+ export function buildAskResponse(catalog, question, options = {}) {
254
+ const route = routeAsk(catalog, question);
255
+ if (!route) return null;
256
+
257
+ const { skill, reason } = route;
258
+ const lines = [
259
+ `Recommended skill: ${skill.name}`,
260
+ `Title: ${skill.title}`,
261
+ `Why: ${reason}`,
262
+ `Resource URI: ${skill.uri}`,
263
+ ];
264
+ if (skill.description) lines.push(`Description: ${skill.description}`);
265
+ if (options.includeSkillContent !== false) {
266
+ lines.push("", "---", "", skill.markdown.trim());
267
+ }
268
+ return lines.join("\n");
269
+ }
270
+
271
+ export function getPrompt(catalog, name, args = {}) {
272
+ const command = catalog.commandByName.get(String(name).toLowerCase());
273
+ if (!command) return null;
274
+
275
+ if (command.name === "ask") {
276
+ const question = String(args.question ?? args.arguments ?? "").trim();
277
+ const routed = question ? buildAskResponse(catalog, question, { includeSkillContent: true }) : null;
278
+ return {
279
+ description: command.description,
280
+ messages: [{
281
+ role: "user",
282
+ content: {
283
+ type: "text",
284
+ text: routed
285
+ ? `${routed}\n\n---\n\nPrompt template:\n\n${command.markdown.trim()}`
286
+ : command.markdown.trim(),
287
+ },
288
+ }],
289
+ };
290
+ }
291
+
292
+ const suffix = String(args.area ?? args.arguments ?? "").trim();
293
+ return {
294
+ description: command.description,
295
+ messages: [{
296
+ role: "user",
297
+ content: {
298
+ type: "text",
299
+ text: suffix ? `${command.markdown.trim()}\n\nArguments: ${suffix}` : command.markdown.trim(),
300
+ },
301
+ }],
302
+ };
303
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from "node:process";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import {
7
+ buildAskResponse,
8
+ findSkill,
9
+ getPrompt,
10
+ listSkills,
11
+ loadPluginCatalog,
12
+ searchSkills,
13
+ } from "./plugin-catalog.mjs";
14
+
15
+ const SERVER_INFO = {
16
+ name: "sqlitedata-swift-mcp",
17
+ version: "1.0.0",
18
+ };
19
+
20
+ const LATEST_PROTOCOL_VERSION = "2025-11-25";
21
+ const SUPPORTED_PROTOCOL_VERSIONS = [
22
+ LATEST_PROTOCOL_VERSION,
23
+ "2025-06-18",
24
+ "2025-03-26",
25
+ "2024-11-05",
26
+ "2024-10-07",
27
+ ];
28
+
29
+ function jsonResponse(id, result) {
30
+ return { jsonrpc: "2.0", id, result };
31
+ }
32
+
33
+ function jsonError(id, code, message, data) {
34
+ return { jsonrpc: "2.0", id, error: { code, message, data } };
35
+ }
36
+
37
+ function makeTextResult(text) {
38
+ return { content: [{ type: "text", text }] };
39
+ }
40
+
41
+ function formatSkill(skill) {
42
+ return [
43
+ `- Name: ${skill.name}`,
44
+ `- Title: ${skill.title}`,
45
+ `- Kind: ${skill.kind ?? "workflow"}`,
46
+ `- Category: ${skill.category ?? "uncategorized"}`,
47
+ `- Resource URI: ${skill.uri}`,
48
+ "",
49
+ "---",
50
+ "",
51
+ skill.markdown.trim(),
52
+ "",
53
+ ].join("\n");
54
+ }
55
+
56
+ function toolDefinitions() {
57
+ return [
58
+ {
59
+ name: "list_skills",
60
+ description: "List the SQLiteData skills exposed by this MCP server.",
61
+ inputSchema: { type: "object", properties: {} },
62
+ },
63
+ {
64
+ name: "search_skills",
65
+ description: "Search SQLiteData skills by name, aliases, and description.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: {
69
+ query: { type: "string", description: "Search text." },
70
+ limit: { type: "integer", minimum: 1, maximum: 20, description: "Max results.", default: 5 },
71
+ },
72
+ required: ["query"],
73
+ },
74
+ },
75
+ {
76
+ name: "get_skill",
77
+ description: "Return the full markdown for a specific SQLiteData skill by name or URI.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {
81
+ name: { type: "string", description: "Skill name, e.g. sqlitedata-swift-core." },
82
+ uri: { type: "string", description: "Skill resource URI." },
83
+ },
84
+ },
85
+ },
86
+ {
87
+ name: "ask",
88
+ description: "Route a natural-language SQLiteData question to the most relevant skill and return its guidance.",
89
+ inputSchema: {
90
+ type: "object",
91
+ properties: {
92
+ question: { type: "string", description: "A natural-language SQLiteData question." },
93
+ includeSkillContent: { type: "boolean", description: "Include the full skill markdown.", default: true },
94
+ },
95
+ required: ["question"],
96
+ },
97
+ },
98
+ ];
99
+ }
100
+
101
+ export function createServer() {
102
+ const pluginCatalog = loadPluginCatalog();
103
+
104
+ function handleRequest(request) {
105
+ const { method, params, id } = request;
106
+
107
+ switch (method) {
108
+ case "initialize": {
109
+ const clientVersion = params?.protocolVersion ?? "2024-11-05";
110
+ const negotiated = SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)
111
+ ? clientVersion
112
+ : LATEST_PROTOCOL_VERSION;
113
+ return jsonResponse(id, {
114
+ protocolVersion: negotiated,
115
+ capabilities: {
116
+ tools: {},
117
+ resources: {},
118
+ prompts: {},
119
+ },
120
+ serverInfo: SERVER_INFO,
121
+ });
122
+ }
123
+
124
+ case "notifications/initialized":
125
+ case "notifications/cancelled":
126
+ return null;
127
+
128
+ case "tools/list":
129
+ return jsonResponse(id, { tools: toolDefinitions() });
130
+
131
+ case "tools/call": {
132
+ const toolName = params?.name;
133
+ const args = params?.arguments ?? {};
134
+
135
+ switch (toolName) {
136
+ case "list_skills":
137
+ return jsonResponse(id, makeTextResult(JSON.stringify(listSkills(pluginCatalog), null, 2)));
138
+
139
+ case "search_skills": {
140
+ const results = searchSkills(pluginCatalog, args.query, args.limit);
141
+ return jsonResponse(id, makeTextResult(JSON.stringify(results, null, 2)));
142
+ }
143
+
144
+ case "get_skill": {
145
+ const skill = findSkill(pluginCatalog, { name: args.name, uri: args.uri });
146
+ if (!skill) return jsonError(id, -32602, `Skill not found: ${args.name ?? args.uri}`);
147
+ return jsonResponse(id, makeTextResult(formatSkill(skill)));
148
+ }
149
+
150
+ case "ask": {
151
+ const response = buildAskResponse(pluginCatalog, args.question, {
152
+ includeSkillContent: args.includeSkillContent,
153
+ });
154
+ if (!response) return jsonError(id, -32602, "Could not route the question to a skill");
155
+ return jsonResponse(id, makeTextResult(response));
156
+ }
157
+
158
+ default:
159
+ return jsonError(id, -32601, `Unknown tool: ${toolName}`);
160
+ }
161
+ }
162
+
163
+ case "resources/list": {
164
+ const resources = pluginCatalog.skills.map((s) => ({
165
+ uri: s.uri,
166
+ name: s.name,
167
+ description: s.description,
168
+ mimeType: "text/markdown",
169
+ }));
170
+ return jsonResponse(id, { resources });
171
+ }
172
+
173
+ case "resources/read": {
174
+ const uri = params?.uri;
175
+ const skill = findSkill(pluginCatalog, { uri });
176
+ if (!skill) return jsonError(id, -32602, `Resource not found: ${uri}`);
177
+ return jsonResponse(id, {
178
+ contents: [{ uri: skill.uri, mimeType: "text/markdown", text: skill.markdown }],
179
+ });
180
+ }
181
+
182
+ case "resources/templates/list":
183
+ return jsonResponse(id, { resourceTemplates: [] });
184
+
185
+ case "prompts/list": {
186
+ const prompts = pluginCatalog.commands.map((c) => ({
187
+ name: c.name,
188
+ description: c.description,
189
+ arguments: c.name === "ask"
190
+ ? [{ name: "question", description: "SQLiteData question", required: true }]
191
+ : [{ name: "area", description: "Optional focus area", required: false }],
192
+ }));
193
+ return jsonResponse(id, { prompts });
194
+ }
195
+
196
+ case "prompts/get": {
197
+ const prompt = getPrompt(pluginCatalog, params?.name, params?.arguments ?? {});
198
+ if (!prompt) return jsonError(id, -32602, `Prompt not found: ${params?.name}`);
199
+ return jsonResponse(id, prompt);
200
+ }
201
+
202
+ default:
203
+ return jsonError(id, -32601, `Method not found: ${method}`);
204
+ }
205
+ }
206
+
207
+ return { handleRequest, pluginCatalog };
208
+ }
209
+
210
+ // ── stdio transport ─────────────────────────────────────────────────────────
211
+
212
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
213
+ const server = createServer();
214
+ let framing = null; // auto-detect
215
+ let buffer = "";
216
+
217
+ function writeMessage(message) {
218
+ const payload = JSON.stringify(message);
219
+ if (framing === "raw-json") {
220
+ process.stdout.write(`${payload}\n`);
221
+ } else {
222
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(payload, "utf8")}\r\n\r\n${payload}`);
223
+ }
224
+ }
225
+
226
+ function processMessage(text) {
227
+ let request;
228
+ try {
229
+ request = JSON.parse(text);
230
+ } catch {
231
+ writeMessage(jsonError(null, -32700, "Parse error"));
232
+ return;
233
+ }
234
+ const response = server.handleRequest(request);
235
+ if (response) writeMessage(response);
236
+ }
237
+
238
+ function findHeaderBoundary(buf) {
239
+ const crlf = buf.indexOf("\r\n\r\n");
240
+ if (crlf !== -1) return { headerEnd: crlf, separatorLength: 4 };
241
+ const lf = buf.indexOf("\n\n");
242
+ if (lf !== -1) return { headerEnd: lf, separatorLength: 2 };
243
+ return null;
244
+ }
245
+
246
+ function drain() {
247
+ while (buffer.length > 0) {
248
+ if (framing === null) {
249
+ // Auto-detect framing from first bytes
250
+ if (buffer.startsWith("{")) {
251
+ framing = "raw-json";
252
+ } else if (buffer.startsWith("Content-Length:") || buffer.startsWith("content-length:")) {
253
+ framing = "content-length";
254
+ } else if (buffer.length < 16) {
255
+ return; // wait for more data
256
+ } else {
257
+ framing = "content-length";
258
+ }
259
+ }
260
+
261
+ if (framing === "raw-json") {
262
+ const newline = buffer.indexOf("\n");
263
+ if (newline === -1) {
264
+ // Try to parse what we have if it's a complete JSON object
265
+ if (buffer.endsWith("}")) {
266
+ const text = buffer;
267
+ buffer = "";
268
+ processMessage(text);
269
+ continue;
270
+ }
271
+ return;
272
+ }
273
+ const line = buffer.slice(0, newline).trim();
274
+ buffer = buffer.slice(newline + 1);
275
+ if (line) processMessage(line);
276
+ continue;
277
+ }
278
+
279
+ // content-length framing
280
+ const boundary = findHeaderBoundary(buffer);
281
+ if (!boundary) return;
282
+
283
+ const header = buffer.slice(0, boundary.headerEnd);
284
+ const match = header.match(/Content-Length:\s*(\d+)/i);
285
+ if (!match) {
286
+ buffer = buffer.slice(boundary.headerEnd + boundary.separatorLength);
287
+ continue;
288
+ }
289
+
290
+ const contentLength = parseInt(match[1], 10);
291
+ const bodyStart = boundary.headerEnd + boundary.separatorLength;
292
+ if (buffer.length < bodyStart + contentLength) return;
293
+
294
+ const body = buffer.slice(bodyStart, bodyStart + contentLength);
295
+ buffer = buffer.slice(bodyStart + contentLength);
296
+ processMessage(body);
297
+ }
298
+ }
299
+
300
+ process.stdin.setEncoding("utf8");
301
+ process.stdin.on("data", (chunk) => {
302
+ buffer += chunk;
303
+ drain();
304
+ });
305
+ }