@skuio/docs-mcp 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.
Files changed (3) hide show
  1. package/README.md +37 -0
  2. package/package.json +13 -0
  3. package/server.mjs +120 -0
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # @skuio/docs-mcp
2
+
3
+ MCP server for the SKU.io API documentation. Gives AI agents three tools over
4
+ the static assets on developer.sku.io — no API keys, no server-side state:
5
+
6
+ | Tool | Does |
7
+ |---|---|
8
+ | `search_docs(query)` | Search guides, API domains, and operation summaries |
9
+ | `fetch_doc(path)` | Fetch any docs page as clean markdown (`.md` twin) |
10
+ | `get_openapi(domain?)` | Domain catalog, or one domain's OpenAPI chunk |
11
+
12
+ Deliberately **not** one-tool-per-operation — 3,200 tools would drown any
13
+ agent. Agents narrow with `search_docs` / `get_openapi`, then read.
14
+
15
+ ## Use with Claude Code
16
+
17
+ From a checkout of this repo:
18
+
19
+ ```bash
20
+ cd mcp && npm install
21
+ claude mcp add skuio-docs -- node /path/to/api-docs/mcp/server.mjs
22
+ ```
23
+
24
+ Once published to npm:
25
+
26
+ ```bash
27
+ claude mcp add skuio-docs -- npx -y @skuio/docs-mcp
28
+ ```
29
+
30
+ ## Publishing
31
+
32
+ `npm publish --access public` from this directory (needs the @skuio npm org).
33
+
34
+ ## Environment
35
+
36
+ - `SKUIO_DOCS_BASE` — override the docs origin (default `https://developer.sku.io`);
37
+ useful against a local `npm run serve` build.
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@skuio/docs-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the SKU.io API documentation (developer.sku.io)",
5
+ "type": "module",
6
+ "bin": { "skuio-docs-mcp": "./server.mjs" },
7
+ "files": ["server.mjs", "README.md"],
8
+ "engines": { "node": ">=18" },
9
+ "dependencies": {
10
+ "@modelcontextprotocol/sdk": "^1.0.0",
11
+ "zod": "^3.23.0"
12
+ }
13
+ }
package/server.mjs ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SKU.io docs MCP server (stdio).
4
+ *
5
+ * Three tools over the static assets on developer.sku.io — no API keys, no
6
+ * state. Deliberately NOT one-tool-per-operation (3,200 tools would drown
7
+ * any agent); agents narrow with search_docs / get_openapi, then read.
8
+ *
9
+ * search_docs(query) — search guides + API domains + operations
10
+ * fetch_doc(path) — fetch a docs page as markdown (.md twin)
11
+ * get_openapi(domain?) — domain catalog, or one domain's spec chunk
12
+ *
13
+ * Usage with Claude Code:
14
+ * claude mcp add skuio-docs -- node /path/to/api-docs/mcp/server.mjs
15
+ * (or once published: claude mcp add skuio-docs -- npx -y @skuio/docs-mcp)
16
+ */
17
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
19
+ import { z } from "zod";
20
+
21
+ const BASE = process.env.SKUIO_DOCS_BASE ?? "https://developer.sku.io";
22
+
23
+ async function fetchText(url) {
24
+ const res = await fetch(url);
25
+ if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`);
26
+ return res.text();
27
+ }
28
+
29
+ let catalogCache = null;
30
+ async function catalog() {
31
+ if (!catalogCache) {
32
+ catalogCache = JSON.parse(await fetchText(`${BASE}/openapi/index.json`));
33
+ }
34
+ return catalogCache;
35
+ }
36
+
37
+ const GUIDES = [
38
+ ["quickstart", "First authenticated API call in five minutes"],
39
+ ["authentication", "Personal Access Tokens, scopes, 401/403 handling"],
40
+ ["pagination", "page/per_page parameters and response envelope"],
41
+ ["filtering-and-sorting", "filter[...] operators and filter_groups trees"],
42
+ ["errors", "Error body shapes per HTTP status, stable codes"],
43
+ ["dates-and-timezones", "UTC datetimes, app-timezone dates"],
44
+ ["rate-limits", "Platform and per-token limits, 429 handling"],
45
+ ["webhooks", "Subscriptions, event catalog, HMAC signature verification"],
46
+ ["api-conventions", "/api vs /api/v2, URL structure, envelopes"],
47
+ ];
48
+
49
+ const server = new McpServer({ name: "skuio-docs", version: "0.1.0" });
50
+
51
+ server.tool(
52
+ "search_docs",
53
+ "Search the SKU.io API documentation: guides, API domains, and operation summaries. Returns matching pages/domains with URLs to fetch next.",
54
+ { query: z.string().describe("Search terms, e.g. 'create sales order' or 'webhook signature'") },
55
+ async ({ query }) => {
56
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
57
+ const score = (text) => {
58
+ const t = text.toLowerCase();
59
+ return terms.reduce((n, term) => n + (t.includes(term) ? 1 : 0), 0);
60
+ };
61
+
62
+ const results = [];
63
+ for (const [slug, desc] of GUIDES) {
64
+ const s = score(`${slug.replace(/-/g, " ")} ${desc}`);
65
+ if (s > 0) results.push({ kind: "guide", title: slug, score: s, url: `${BASE}/docs/guides/${slug}.md`, description: desc });
66
+ }
67
+ const cat = await catalog();
68
+ for (const t of cat.tags) {
69
+ const s = score(t.tag);
70
+ if (s > 0) results.push({ kind: "api-domain", title: t.tag, score: s + 0.5, url: t.url, operations: t.operations, group: t.group });
71
+ }
72
+ // operation-level: scan summaries in the top-matching domains (cheap: only if a domain matched or nothing matched yet)
73
+ const domains = results.filter((r) => r.kind === "api-domain").slice(0, 3);
74
+ for (const d of domains) {
75
+ const chunk = await fetchText(d.url);
76
+ for (const line of chunk.split("\n")) {
77
+ const m = line.match(/^ {6}summary: (.+)$/);
78
+ if (m && score(m[1]) >= Math.max(1, terms.length - 1)) {
79
+ results.push({ kind: "operation", title: m[1].trim(), score: score(m[1]), domain: d.title, hint: `fetch get_openapi domain='${d.title}' and search for this summary` });
80
+ }
81
+ }
82
+ }
83
+ results.sort((a, b) => b.score - a.score);
84
+ return { content: [{ type: "text", text: JSON.stringify(results.slice(0, 20), null, 2) }] };
85
+ }
86
+ );
87
+
88
+ server.tool(
89
+ "fetch_doc",
90
+ "Fetch a developer.sku.io docs page as clean markdown. Pass a path like /docs/guides/quickstart or /docs/api/introduction (the .md twin is fetched automatically).",
91
+ { path: z.string().describe("Docs path, e.g. /docs/guides/webhooks") },
92
+ async ({ path }) => {
93
+ let p = path.startsWith("/") ? path : `/${path}`;
94
+ if (!p.endsWith(".md")) p += ".md";
95
+ const text = await fetchText(`${BASE}${p}`);
96
+ return { content: [{ type: "text", text }] };
97
+ }
98
+ );
99
+
100
+ server.tool(
101
+ "get_openapi",
102
+ "Without arguments: the catalog of API domains (tag, group, operation count, chunk URL). With a domain: that domain's full OpenAPI chunk (YAML) — every path, parameter, and example for that domain.",
103
+ { domain: z.string().optional().describe("Domain/tag name from the catalog, e.g. 'Sales Orders' or 'Amazon'") },
104
+ async ({ domain }) => {
105
+ const cat = await catalog();
106
+ if (!domain) {
107
+ return { content: [{ type: "text", text: JSON.stringify(cat, null, 2) }] };
108
+ }
109
+ const hit = cat.tags.find((t) => t.tag.toLowerCase() === domain.toLowerCase());
110
+ if (!hit) {
111
+ const names = cat.tags.map((t) => t.tag).join(", ");
112
+ return { content: [{ type: "text", text: `Unknown domain '${domain}'. Available: ${names}` }] };
113
+ }
114
+ const yamlText = await fetchText(hit.url);
115
+ return { content: [{ type: "text", text: yamlText }] };
116
+ }
117
+ );
118
+
119
+ const transport = new StdioServerTransport();
120
+ await server.connect(transport);