@jsswift/knowledge-mcp 0.0.0-stage → 0.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 CHANGED
@@ -1,3 +1,24 @@
1
- # Temporary Holding Version
1
+ # JSswift knowledge MCP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Read the documentation of the `jsswift` package installed in your application. Requires Node.js 20+ and a JSswift release containing bundled documentation. No browser runtime is executed.
4
+
5
+ Requires JSswift 1.0.44 or newer for bundled documentation. Configure an MCP stdio client with:
6
+
7
+ ```json
8
+ {
9
+ "mcpServers": {
10
+ "jsswift": {
11
+ "command": "npx",
12
+ "args": ["-y", "--fetch-retries=0", "@jsswift/knowledge-mcp@latest"]
13
+ }
14
+ }
15
+ }
16
+ ```
17
+
18
+ The client must start the server in the app directory. Otherwise append `--project`, `/absolute/path/to/app` to `args`. Node package resolution supports ancestor node_modules, hoisted dependencies and package symlinks; for a monorepo with different app versions, point to the desired app. Each server connection serves one app.
19
+
20
+ Search with `jsswift_search`, then read the returned IDs with `jsswift_read`. Both tools report the installed framework version. Resources expose the shared map and all indexed documents. Application APIs use `_.Toggle`, `_.signal`, `_.effect`, `_.computed`, `_.untracked`, and `_.batch`.
21
+
22
+ Once the MCP package and its dependencies are cached or installed locally, documentation reads work offline. Initial `npx` execution requires downloading the package. Reconnect after upgrading JSswift. Missing documentation and version mismatches cause startup errors instead of silently using another release's documentation.
23
+
24
+ For framework documentation development only, use `--docs /path/to/JSswift/documentions`; responses explicitly report `development` rather than an installed release.
package/dist/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carlos Malleux
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.mjs
4
+ import { McpServer } from "@modelcontextprotocol/server";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
6
+ import * as z from "zod/v4";
7
+
8
+ // ../knowledge.mjs
9
+ import { readFile, realpath, readdir } from "node:fs/promises";
10
+ import { fileURLToPath } from "node:url";
11
+ import path from "node:path";
12
+ var defaultRoot = fileURLToPath(new URL("../documentions/", import.meta.url));
13
+ var tokens = (value) => String(value).normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().match(/[a-z0-9]+/g) ?? [];
14
+ async function loadKnowledge(root = defaultRoot) {
15
+ const docsRoot = await realpath(root);
16
+ const map = JSON.parse(await readFile(path.join(docsRoot, "knowledge-map.json"), "utf8"));
17
+ if (map.schemaVersion !== 1 || map.scope !== "application-development") throw new Error("Unsupported knowledge map");
18
+ if (!Array.isArray(map.topics) || !Array.isArray(map.components)) throw new Error("Invalid knowledge collections");
19
+ const topicIds = /* @__PURE__ */ new Set();
20
+ for (const topic of map.topics) {
21
+ if (typeof topic.id !== "string" || topicIds.has(topic.id) || typeof topic.title !== "string" || !Array.isArray(topic.paths) || !topic.paths.length || !Array.isArray(topic.keywords) || topic.keywords.some((word) => typeof word !== "string")) throw new Error("Invalid or duplicate topic");
22
+ topicIds.add(topic.id);
23
+ }
24
+ for (const component of map.components) {
25
+ if (typeof component.id !== "string" || typeof component.name !== "string" || typeof component.summary !== "string" || !Array.isArray(component.keywords) || component.keywords.some((word) => typeof word !== "string") || !Array.isArray(component.topics) || !component.topics.length || component.topics.some((id) => !topicIds.has(id))) throw new Error("Invalid component entry");
26
+ }
27
+ const entries = /* @__PURE__ */ new Map();
28
+ const add = (entry) => {
29
+ if (entries.has(entry.id)) throw new Error(`Duplicate document ID: ${entry.id}`);
30
+ entries.set(entry.id, entry);
31
+ };
32
+ add({ id: "guide:index", kind: "guide", title: "Application documentation", path: map.entrypoint, keywords: ["start", "index"] });
33
+ for (const topic of map.topics) {
34
+ for (const docPath of topic.paths) {
35
+ const id = `guide:${docPath.replace(/\.md$/, "")}`;
36
+ const existing = entries.get(id);
37
+ if (existing) existing.keywords.push(...topic.keywords);
38
+ else add({ id, kind: "guide", title: topic.title, path: docPath, keywords: [...topic.keywords] });
39
+ }
40
+ }
41
+ for (const component of map.components) {
42
+ add({
43
+ id: `component:${component.id}`,
44
+ kind: "component",
45
+ title: component.name,
46
+ path: component.path,
47
+ summary: component.summary,
48
+ keywords: component.keywords,
49
+ topics: component.topics
50
+ });
51
+ }
52
+ async function safePath(relativePath) {
53
+ if (typeof relativePath !== "string" || path.isAbsolute(relativePath) || relativePath.split(/[\\/]/).includes("..")) {
54
+ throw new Error("Invalid documentation path");
55
+ }
56
+ const resolved = await realpath(path.join(docsRoot, relativePath));
57
+ if (!resolved.startsWith(docsRoot + path.sep)) throw new Error("Document outside documentation root");
58
+ return resolved;
59
+ }
60
+ for (const entry of entries.values()) {
61
+ if (!entry.path.endsWith(".md")) throw new Error(`Invalid document extension: ${entry.path}`);
62
+ const text = await readFile(await safePath(entry.path), "utf8");
63
+ if (entry.kind === "guide") entry.title = text.split("\n")[0].replace(/^# /, "");
64
+ if (entry.kind === "component") {
65
+ const block = text.match(/```json\s*([\s\S]*?)```/);
66
+ if (!block) throw new Error(`Missing component metadata: ${entry.path}`);
67
+ const metadata = JSON.parse(block[1]);
68
+ if (!metadata.signature?.startsWith(`_.${entry.title}(`) || !entry.keywords.includes(`_.${entry.title}`) || entry.keywords.some((word) => word.startsWith("JSswift."))) {
69
+ throw new Error(`Noncanonical application API: ${entry.path}; use _.${entry.title}`);
70
+ }
71
+ if (text.split("\n")[0] !== `# ${entry.title}` || (metadata.description ?? "") !== entry.summary) {
72
+ throw new Error(`Stale component map: ${entry.path}; run npm run knowledge:refresh`);
73
+ }
74
+ }
75
+ }
76
+ const files = (await readdir(path.join(docsRoot, "components"))).filter((name) => name.endsWith(".md") && name !== "README.md");
77
+ const indexed = new Set(map.components.map((component) => component.path));
78
+ if (files.length !== indexed.size || files.some((name) => !indexed.has(`components/${name}`))) {
79
+ throw new Error("Component map coverage mismatch; run npm run knowledge:refresh");
80
+ }
81
+ return {
82
+ map,
83
+ list: () => [...entries.values()].map(({ keywords, ...entry }) => entry),
84
+ search(query, limit = 6) {
85
+ if (typeof query !== "string" || query.length > 500) throw new Error("Query must be a string of at most 500 characters");
86
+ if (!Number.isInteger(limit) || limit < 1 || limit > 20) throw new Error("Limit must be from 1 to 20");
87
+ const words = tokens(query);
88
+ if (!words.length) return [];
89
+ const ranked = [...entries.values()].map((entry) => {
90
+ const names = tokens(`${entry.id} ${entry.title}`);
91
+ const keywords = tokens(entry.keywords.join(" "));
92
+ const summary = tokens(entry.summary ?? "");
93
+ const score = words.reduce((sum, word) => sum + (names.includes(word) ? 12 : keywords.includes(word) ? 6 : summary.includes(word) ? 1 : 0), 0);
94
+ const { keywords: ignored, ...result } = entry;
95
+ return { ...result, score };
96
+ }).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
97
+ return ranked.slice(0, limit);
98
+ },
99
+ async read(id) {
100
+ const entry = entries.get(id);
101
+ if (!entry) throw new Error("Unknown document ID; use jsswift_search to find a documented ID");
102
+ return { id, path: entry.path, text: await readFile(await safePath(entry.path), "utf8") };
103
+ }
104
+ };
105
+ }
106
+
107
+ // src/project.mjs
108
+ import { createRequire } from "node:module";
109
+ import { readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
110
+ import path2 from "node:path";
111
+ async function resolveProject(project = process.cwd()) {
112
+ const root = await realpath2(path2.resolve(project));
113
+ const require2 = createRequire(path2.join(root, "package.json"));
114
+ let mapPath, manifestPath;
115
+ try {
116
+ mapPath = require2.resolve("jsswift/knowledge-map.json");
117
+ manifestPath = require2.resolve("jsswift/package.json");
118
+ } catch {
119
+ throw new Error(`No JSswift package with bundled documentation found from ${root}. Install a release containing knowledge-map.json, or use --project /path/to/app.`);
120
+ }
121
+ const manifest = JSON.parse(await readFile2(manifestPath, "utf8"));
122
+ const map = JSON.parse(await readFile2(mapPath, "utf8"));
123
+ if (manifest.name !== "jsswift" || map.packageVersion !== manifest.version) {
124
+ throw new Error("JSswift documentation version does not match the installed package");
125
+ }
126
+ return { project: root, version: manifest.version, docsRoot: path2.dirname(mapPath) };
127
+ }
128
+ function parseArgs(args) {
129
+ const options2 = { project: process.cwd() };
130
+ for (let i = 0; i < args.length; i++) {
131
+ const key = args[i];
132
+ if (!["--project", "--docs"].includes(key) || !args[i + 1] || args[i + 1].startsWith("--")) {
133
+ throw new Error("Usage: jsswift-mcp [--project /path/to/app] [--docs /path/to/development/documentions]");
134
+ }
135
+ options2[key.slice(2)] = path2.resolve(args[++i]);
136
+ }
137
+ return options2;
138
+ }
139
+
140
+ // src/server.mjs
141
+ var options = parseArgs(process.argv.slice(2));
142
+ var context = options.docs ? { project: options.project, version: "development", docsRoot: options.docs } : await resolveProject(options.project);
143
+ var knowledge = await loadKnowledge(context.docsRoot);
144
+ var server = new McpServer({ name: "jsswift-knowledge", version: "0.1.0" }, {
145
+ instructions: `Serving JSswift ${context.version} from project ${context.project}. Prefer _.Button for buttons. Use _.Component and _.signal, _.effect, _.computed, _.untracked, _.batch. Search for the feature and read selected contracts before coding. This server covers application development only.`
146
+ });
147
+ var annotations = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
148
+ var jsonResult = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }], structuredContent: value });
149
+ server.registerTool("jsswift_search", {
150
+ title: "Find JSswift application documentation",
151
+ description: "Find focused guides and component contracts by feature, API name, or Italian/English keywords. Returns document IDs and paths. Read matching documents with jsswift_read before using APIs; no results means no indexed match, not permission to invent an API.",
152
+ inputSchema: z.object({ query: z.string().trim().min(1).max(500), limit: z.number().int().min(1).max(20).default(6) }),
153
+ annotations
154
+ }, async ({ query, limit }) => jsonResult({ project: context.project, version: context.version, results: knowledge.search(query, limit) }));
155
+ server.registerTool("jsswift_read", {
156
+ title: "Read a JSswift guide or exact component contract",
157
+ description: "Read one complete indexed application document using its ID from jsswift_search or the resource list. Does not accept filesystem paths.",
158
+ inputSchema: z.object({ id: z.string().min(1).max(200) }),
159
+ annotations
160
+ }, async ({ id }) => {
161
+ try {
162
+ return jsonResult({ project: context.project, version: context.version, ...await knowledge.read(id) });
163
+ } catch {
164
+ return { isError: true, content: [{ type: "text", text: "Document unavailable or unknown ID. Use jsswift_search to select an indexed document." }] };
165
+ }
166
+ });
167
+ server.registerResource("knowledge-map", "jsswift://knowledge/map", {
168
+ title: "JSswift application knowledge map",
169
+ mimeType: "application/json"
170
+ }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: "application/json", text: JSON.stringify(knowledge.map) }] }));
171
+ for (const document of knowledge.list()) {
172
+ server.registerResource(document.id, `jsswift://docs/${encodeURIComponent(document.id)}`, {
173
+ title: document.title,
174
+ description: document.summary ?? document.path,
175
+ mimeType: "text/markdown"
176
+ }, async (uri) => ({ contents: [{ uri: uri.href, mimeType: "text/markdown", text: (await knowledge.read(document.id)).text }] }));
177
+ }
178
+ await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
2
  "name": "@jsswift/knowledge-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Read-only MCP for the installed JSswift version's documentation",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "scripts": {
10
+ "start": "node dist/server.mjs",
11
+ "test": "node --test tests/*.test.mjs",
12
+ "build": "node ../../tools/build-knowledge-mcp.mjs",
13
+ "prepack": "npm run build"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/server": "2.3.1",
17
+ "zod": "4.6.5"
18
+ },
19
+ "devDependencies": {
20
+ "@modelcontextprotocol/client": "2.3.1"
21
+ },
22
+ "license": "MIT",
23
+ "bin": {
24
+ "jsswift-mcp": "dist/server.mjs"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/camavi/JSswift.git",
33
+ "directory": "ai/mcp"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }