@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.
- package/README.md +37 -0
- package/package.json +13 -0
- 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);
|