@llmpedia/mcp 0.2.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 +66 -0
  2. package/dist/index.js +234 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # @llmpedia/mcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that lets your own AI agent
4
+ (Claude, Codex, …) create and maintain your [LLMpedia](https://api.llmpedia.org) wiki
5
+ page. It is a thin layer over the LLMpedia public REST API — every tool is an
6
+ authenticated HTTP call using your API key. No database access, no secrets beyond
7
+ your key.
8
+
9
+ ## Setup
10
+
11
+ Get your API key from **Dashboard → Connect your AI agent** (`/dashboard/api`).
12
+
13
+ Add to your agent's MCP config:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "llmpedia": {
19
+ "command": "npx",
20
+ "args": ["-y", "@llmpedia/mcp"],
21
+ "env": {
22
+ "LLMPEDIA_API_KEY": "llmp_sk_...",
23
+ "LLMPEDIA_BASE_URL": "https://api.llmpedia.org"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ `LLMPEDIA_BASE_URL` is optional (defaults to `https://api.llmpedia.org`).
31
+
32
+ ## Tools
33
+
34
+ ### Page tools
35
+
36
+ | Tool | Description |
37
+ |---|---|
38
+ | `whoami` | Account, plan, topic quota, write budget. |
39
+ | `list_pages` | Your pages (id, slug, title, status, language). |
40
+ | `get_page` | One page's full structured content (by id or slug). |
41
+ | `create_page` | Create a page from researched, structured content. |
42
+ | `update_page` | Replace a page's content (maintenance). |
43
+ | `publish_page` | Validate + publish. |
44
+ | `unpublish_page` | Back to draft. |
45
+
46
+ ### Post tools
47
+
48
+ | Tool | Description |
49
+ |---|---|
50
+ | `list_posts` | A page's blog/news/update posts (id = page id or slug). |
51
+ | `get_post` | One post's full content (by post id). |
52
+ | `create_post` | Create a blog/news/update post under a page, structured for AEO/GEO. |
53
+ | `update_post` | Replace a post's content (slug/kind stay fixed to the target). |
54
+ | `publish_post` | Validate + publish. |
55
+ | `unpublish_post` | Back to draft. |
56
+
57
+ Both the page and post content schemas are documented (with examples) at
58
+ `<base-url>/api/v1/docs` — fetch it to see every field.
59
+
60
+ ## Develop
61
+
62
+ ```bash
63
+ npm install
64
+ npm run build # compile to dist/
65
+ LLMPEDIA_API_KEY=llmp_sk_... npm run dev
66
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ /**
6
+ * LLMpedia MCP server — thin tool layer over the public REST API (/api/v1). It
7
+ * holds NO database access and no app internals: every tool is an authenticated
8
+ * HTTP call using the user's API key. Configure it with:
9
+ *
10
+ * LLMPEDIA_API_KEY (required) the user's llmpedia_key_… key
11
+ * LLMPEDIA_BASE_URL (optional) defaults to https://llmpedia.org
12
+ */
13
+ const API_KEY = process.env.LLMPEDIA_API_KEY;
14
+ const BASE_URL = (process.env.LLMPEDIA_BASE_URL || "https://api.llmpedia.org").replace(/\/+$/, "");
15
+ if (!API_KEY) {
16
+ console.error("LLMPEDIA_API_KEY is required. Get your key + docs at <site>/dashboard.");
17
+ process.exit(1);
18
+ }
19
+ async function api(method, path, body) {
20
+ const res = await fetch(`${BASE_URL}/api/v1${path}`, {
21
+ method,
22
+ headers: {
23
+ Authorization: `Bearer ${API_KEY}`,
24
+ "content-type": "application/json",
25
+ },
26
+ body: body === undefined ? undefined : JSON.stringify(body),
27
+ });
28
+ const text = await res.text();
29
+ let data = text;
30
+ try {
31
+ data = JSON.parse(text);
32
+ }
33
+ catch {
34
+ /* keep raw text */
35
+ }
36
+ return { ok: res.ok, status: res.status, data };
37
+ }
38
+ function result(r) {
39
+ return {
40
+ content: [{ type: "text", text: JSON.stringify(r.data, null, 2) }],
41
+ isError: !r.ok,
42
+ };
43
+ }
44
+ function withLanguage(path, language) {
45
+ return language ? `${path}?language=${encodeURIComponent(language)}` : path;
46
+ }
47
+ /** The page content object (mirrors GET /api/v1/docs). Lenient: extra keys pass. */
48
+ const pageObject = z
49
+ .object({
50
+ title: z.string().describe("Canonical entity name."),
51
+ slug: z.string().optional().describe("URL slug; defaults from the title."),
52
+ language: z.string().optional().describe('Edition: "en" or "ko" (default "en").'),
53
+ entityType: z.string().optional().describe('Primary Schema.org type, e.g. "Organization".'),
54
+ schemaTypes: z.array(z.string()).optional(),
55
+ summary: z.string().describe("3–5 sentence canonical, citable summary."),
56
+ canonicalFacts: z
57
+ .array(z.object({
58
+ label: z.string(),
59
+ value: z.string(),
60
+ sourceUrl: z.string().nullable().optional(),
61
+ }))
62
+ .optional()
63
+ .describe("Quantified facts, each ideally with a deep source URL."),
64
+ sections: z
65
+ .array(z.object({
66
+ heading: z.string(),
67
+ body: z.string(),
68
+ sortOrder: z.number().optional(),
69
+ }))
70
+ .optional()
71
+ .describe("Prose sections (answer-first)."),
72
+ faqs: z
73
+ .array(z.object({ question: z.string(), answer: z.string() }))
74
+ .optional(),
75
+ aliases: z.array(z.string()).optional(),
76
+ sameAs: z.array(z.string()).optional().describe("Authoritative identity URLs."),
77
+ references: z
78
+ .array(z.object({
79
+ url: z.string(),
80
+ title: z.string().optional(),
81
+ supports: z.string().optional(),
82
+ }))
83
+ .optional()
84
+ .describe("Deep source URLs you used."),
85
+ disambiguation: z.array(z.string()).optional(),
86
+ seo: z
87
+ .object({ title: z.string().optional(), description: z.string().optional() })
88
+ .optional(),
89
+ aeo: z
90
+ .object({
91
+ answerSummary: z.string().optional(),
92
+ questionTargets: z.array(z.string()).optional(),
93
+ })
94
+ .optional(),
95
+ geo: z
96
+ .object({
97
+ llmSummary: z.string().optional(),
98
+ citationReadyClaims: z.array(z.string()).optional(),
99
+ })
100
+ .optional(),
101
+ schemaProperties: z.record(z.string(), z.any()).optional(),
102
+ sourceUrls: z.array(z.string()).optional(),
103
+ translationGroupId: z.string().optional(),
104
+ })
105
+ .passthrough();
106
+ const server = new McpServer({ name: "llmpedia", version: "0.1.0" });
107
+ server.registerTool("whoami", {
108
+ title: "Who am I",
109
+ description: "Return the authenticated account, plan, topic quota, and write budget.",
110
+ inputSchema: {},
111
+ }, async () => result(await api("GET", "/me")));
112
+ server.registerTool("list_pages", {
113
+ title: "List pages",
114
+ description: "List the pages you own (id, slug, title, status, language).",
115
+ inputSchema: {},
116
+ }, async () => result(await api("GET", "/pages")));
117
+ server.registerTool("get_page", {
118
+ title: "Get page",
119
+ description: "Read one page's full structured content by id or by slug.",
120
+ inputSchema: {
121
+ id: z.string().describe("Page id (uuid) or slug."),
122
+ language: z.string().optional().describe('Required only when id is a slug. "en" | "ko".'),
123
+ },
124
+ }, async ({ id, language }) => result(await api("GET", withLanguage(`/pages/${encodeURIComponent(id)}`, language))));
125
+ server.registerTool("create_page", {
126
+ title: "Create page",
127
+ description: "Create a page from researched, structured content. See whoami for quota first.",
128
+ inputSchema: { page: pageObject },
129
+ }, async ({ page }) => result(await api("POST", "/pages", page)));
130
+ server.registerTool("update_page", {
131
+ title: "Update page",
132
+ description: "Replace a page's content (for maintenance). slug/language stay fixed.",
133
+ inputSchema: {
134
+ id: z.string().describe("Page id (uuid) or slug."),
135
+ language: z.string().optional().describe('Required only when id is a slug.'),
136
+ page: pageObject,
137
+ },
138
+ }, async ({ id, language, page }) => result(await api("PUT", withLanguage(`/pages/${encodeURIComponent(id)}`, language), page)));
139
+ server.registerTool("publish_page", {
140
+ title: "Publish page",
141
+ description: "Validate and publish a page so AI assistants can read and cite it.",
142
+ inputSchema: {
143
+ id: z.string().describe("Page id (uuid) or slug."),
144
+ language: z.string().optional(),
145
+ },
146
+ }, async ({ id, language }) => result(await api("POST", withLanguage(`/pages/${encodeURIComponent(id)}/publish`, language))));
147
+ server.registerTool("unpublish_page", {
148
+ title: "Unpublish page",
149
+ description: "Move a published page back to draft.",
150
+ inputSchema: {
151
+ id: z.string().describe("Page id (uuid) or slug."),
152
+ language: z.string().optional(),
153
+ },
154
+ }, async ({ id, language }) => result(await api("POST", withLanguage(`/pages/${encodeURIComponent(id)}/unpublish`, language))));
155
+ /** Timeline post (blog/news/update). Lenient: extra keys pass. */
156
+ const postObject = z
157
+ .object({
158
+ kind: z.enum(["blog", "news", "update"]).optional().describe('Post kind (default "blog").'),
159
+ slug: z.string().optional().describe("URL slug; defaults from the title."),
160
+ title: z.string().describe("Headline."),
161
+ summary: z.string().optional().describe("Short dek / citable abstract."),
162
+ body: z.string().describe("Markdown body."),
163
+ faqs: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
164
+ sources: z
165
+ .array(z.object({ label: z.string().optional(), url: z.string() }))
166
+ .optional(),
167
+ entityType: z.string().optional().describe("Overrides kind→@type."),
168
+ schemaTypes: z.array(z.string()).optional(),
169
+ schemaProperties: z.record(z.string(), z.any()).optional(),
170
+ seo: z
171
+ .object({ title: z.string().optional(), description: z.string().optional() })
172
+ .optional(),
173
+ aeo: z.object({ answerSummary: z.string().optional() }).optional(),
174
+ geo: z.object({ llmSummary: z.string().optional() }).optional(),
175
+ byline: z.string().optional(),
176
+ translationGroupId: z.string().optional(),
177
+ })
178
+ .passthrough();
179
+ function postsPath(pageId, language, kind) {
180
+ const qs = new URLSearchParams();
181
+ if (language)
182
+ qs.set("language", language);
183
+ if (kind)
184
+ qs.set("kind", kind);
185
+ const q = qs.toString();
186
+ return `/pages/${encodeURIComponent(pageId)}/posts${q ? `?${q}` : ""}`;
187
+ }
188
+ server.registerTool("list_posts", {
189
+ title: "List posts",
190
+ description: "List blog/news/update posts under a page (id = uuid or slug).",
191
+ inputSchema: {
192
+ pageId: z.string().describe("Parent page id (uuid) or slug."),
193
+ language: z.string().optional().describe('Required only when pageId is a slug.'),
194
+ kind: z.enum(["blog", "news", "update"]).optional(),
195
+ },
196
+ }, async ({ pageId, language, kind }) => result(await api("GET", postsPath(pageId, language, kind))));
197
+ server.registerTool("get_post", {
198
+ title: "Get post",
199
+ description: "Read one post's full content by post id (uuid).",
200
+ inputSchema: { postId: z.string().describe("Post id (uuid).") },
201
+ }, async ({ postId }) => result(await api("GET", `/posts/${encodeURIComponent(postId)}`)));
202
+ server.registerTool("create_post", {
203
+ title: "Create post",
204
+ description: "Create a blog/news/update post under a page, structured for AEO/GEO.",
205
+ inputSchema: {
206
+ pageId: z.string().describe("Parent page id (uuid) or slug."),
207
+ language: z.string().optional().describe('Required only when pageId is a slug.'),
208
+ post: postObject,
209
+ },
210
+ }, async ({ pageId, language, post }) => result(await api("POST", postsPath(pageId, language), post)));
211
+ server.registerTool("update_post", {
212
+ title: "Update post",
213
+ description: "Replace a post's content (slug/kind stay fixed to the target).",
214
+ inputSchema: { postId: z.string().describe("Post id (uuid)."), post: postObject },
215
+ }, async ({ postId, post }) => result(await api("PUT", `/posts/${encodeURIComponent(postId)}`, post)));
216
+ server.registerTool("publish_post", {
217
+ title: "Publish post",
218
+ description: "Validate and publish a post so AI assistants can read and cite it.",
219
+ inputSchema: { postId: z.string().describe("Post id (uuid).") },
220
+ }, async ({ postId }) => result(await api("POST", `/posts/${encodeURIComponent(postId)}/publish`)));
221
+ server.registerTool("unpublish_post", {
222
+ title: "Unpublish post",
223
+ description: "Move a published post back to draft.",
224
+ inputSchema: { postId: z.string().describe("Post id (uuid).") },
225
+ }, async ({ postId }) => result(await api("POST", `/posts/${encodeURIComponent(postId)}/unpublish`)));
226
+ async function main() {
227
+ const transport = new StdioServerTransport();
228
+ await server.connect(transport);
229
+ console.error(`llmpedia-mcp connected (base: ${BASE_URL})`);
230
+ }
231
+ main().catch((err) => {
232
+ console.error("llmpedia-mcp failed to start:", err);
233
+ process.exit(1);
234
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@llmpedia/mcp",
3
+ "version": "0.2.0",
4
+ "description": "MCP server for creating and maintaining LLMpedia wiki pages with your own AI agent.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "llmpedia-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc -p tsconfig.json",
19
+ "dev": "node --import tsx src/index.ts",
20
+ "prepare": "npm run build"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.12.0",
24
+ "zod": "^3.23.8"
25
+ },
26
+ "devDependencies": {
27
+ "tsx": "^4.19.0",
28
+ "typescript": "^5.5.0"
29
+ }
30
+ }