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 +61 -0
- package/bin/sqlitedata-swift-mcp.mjs +2 -0
- package/package.json +18 -0
- package/src/plugin-catalog.mjs +303 -0
- package/src/server.mjs +305 -0
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
|
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
|
+
}
|