@mrfentmen/catfact-mcp 0.0.0-stage → 1.0.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 +32 -2
- package/dist/api.d.ts +14 -0
- package/dist/api.js +91 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +43 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Cat Fact
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Cat facts and breed data from catfact.ninja - a random fact, fact browsing by page, and the cat breed list, keyless.
|
|
4
|
+
|
|
5
|
+
Keyless — no signup, no API key, no environment variables.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @mrfentmen/catfact-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"catfact-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/catfact-mcp"] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `random_cat_fact` | One random cat fact. |
|
|
28
|
+
| `cat_breeds` | The cat breed list with country of origin, coat and pattern. |
|
|
29
|
+
| `list_facts` | Browse the cat fact database page by page. |
|
|
30
|
+
|
|
31
|
+
## Data source
|
|
32
|
+
|
|
33
|
+
catfact.ninja, a keyless cat fact and breed API.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class SourceError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare function getJson(path: string, accept?: string, timeoutMs?: number): Promise<any>;
|
|
4
|
+
export declare function clampLimit(n: number | undefined, fallback: number, max: number): number;
|
|
5
|
+
export declare function clean(s: unknown): string;
|
|
6
|
+
export declare function randomCatFact(): Promise<string>;
|
|
7
|
+
export declare function catBreeds(a: {
|
|
8
|
+
limit?: number;
|
|
9
|
+
page?: number;
|
|
10
|
+
}): Promise<string>;
|
|
11
|
+
export declare function listFacts(a: {
|
|
12
|
+
limit?: number;
|
|
13
|
+
page?: number;
|
|
14
|
+
}): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cat Fact — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
const BASE = "https://catfact.ninja";
|
|
5
|
+
const SOURCE = "Cat Fact";
|
|
6
|
+
export class SourceError extends Error {
|
|
7
|
+
}
|
|
8
|
+
async function request(path, accept = "application/json", timeoutMs = 25000) {
|
|
9
|
+
const ctrl = new AbortController();
|
|
10
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch(path.startsWith("http") ? path : BASE + path, {
|
|
13
|
+
signal: ctrl.signal,
|
|
14
|
+
headers: { "user-agent": "catfact-mcp", accept },
|
|
15
|
+
});
|
|
16
|
+
if (!res.ok)
|
|
17
|
+
throw new SourceError(`${SOURCE} responded ${res.status}`);
|
|
18
|
+
return res;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
if (e instanceof SourceError)
|
|
22
|
+
throw e;
|
|
23
|
+
if (e.name === "AbortError")
|
|
24
|
+
throw new SourceError(`${SOURCE} request timed out`);
|
|
25
|
+
throw new SourceError(`${SOURCE} request failed: ${e.message}`);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function getJson(path, accept = "application/json", timeoutMs = 25000) {
|
|
32
|
+
const res = await request(path, accept, timeoutMs);
|
|
33
|
+
const body = await res.text();
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(body);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new SourceError(`${SOURCE} returned malformed JSON`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function clampLimit(n, fallback, max) {
|
|
42
|
+
const v = n ?? fallback;
|
|
43
|
+
if (!Number.isInteger(v) || v < 1 || v > max)
|
|
44
|
+
throw new SourceError(`limit must be between 1 and ${max}`);
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
47
|
+
export function clean(s) {
|
|
48
|
+
return String(s ?? "").replace(/\s+/g, " ").trim();
|
|
49
|
+
}
|
|
50
|
+
export async function randomCatFact() {
|
|
51
|
+
const body = await getJson("/fact", "application/json", 25000);
|
|
52
|
+
const fact = clean(body?.fact);
|
|
53
|
+
if (!fact)
|
|
54
|
+
throw new SourceError("catfact.ninja returned no fact");
|
|
55
|
+
return `${fact}\n\n(${fact.length} characters)`;
|
|
56
|
+
}
|
|
57
|
+
export async function catBreeds(a) {
|
|
58
|
+
const limit = clampLimit(a.limit, 10, 50);
|
|
59
|
+
const page = Math.floor(a.page ?? 1);
|
|
60
|
+
if (!Number.isInteger(page) || page < 1 || page > 50)
|
|
61
|
+
throw new SourceError("page must be between 1 and 50");
|
|
62
|
+
const body = await getJson(`/breeds?limit=${limit}&page=${page}`, "application/json", 25000);
|
|
63
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
64
|
+
if (!rows.length)
|
|
65
|
+
throw new SourceError("catfact.ninja returned no breeds");
|
|
66
|
+
const start = (Number(body?.current_page ?? page) - 1) * limit;
|
|
67
|
+
return [
|
|
68
|
+
`${body?.total ?? rows.length} breed(s) on record (page ${body?.current_page ?? page} of ${body?.last_page ?? "?"}), showing ${rows.length}:`,
|
|
69
|
+
"",
|
|
70
|
+
...rows.map((r, i) => {
|
|
71
|
+
const bits = [clean(r?.country), clean(r?.coat) ? `coat ${clean(r.coat)}` : "", clean(r?.pattern)].filter(Boolean);
|
|
72
|
+
return `${String(start + i + 1).padStart(3)}. ${clean(r?.breed).padEnd(24)} ${bits.join(" · ")}`;
|
|
73
|
+
}),
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
export async function listFacts(a) {
|
|
77
|
+
const limit = clampLimit(a.limit, 10, 50);
|
|
78
|
+
const page = Math.floor(a.page ?? 1);
|
|
79
|
+
if (!Number.isInteger(page) || page < 1 || page > 50)
|
|
80
|
+
throw new SourceError("page must be between 1 and 50");
|
|
81
|
+
const body = await getJson(`/facts?limit=${limit}&page=${page}`, "application/json", 25000);
|
|
82
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
83
|
+
if (!rows.length)
|
|
84
|
+
throw new SourceError("catfact.ninja returned no facts");
|
|
85
|
+
const start = (Number(body?.current_page ?? page) - 1) * limit;
|
|
86
|
+
return [
|
|
87
|
+
`${body?.total ?? rows.length} fact(s) on record (page ${body?.current_page ?? page} of ${body?.last_page ?? "?"}), showing ${rows.length}:`,
|
|
88
|
+
"",
|
|
89
|
+
...rows.map((r, i) => `${String(start + i + 1).padStart(4)}. ${clean(r?.fact)}`),
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("catfact-mcp running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { catBreeds, listFacts, randomCatFact, SourceError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
function errorMessage(e) {
|
|
6
|
+
if (e instanceof SourceError || e instanceof Error)
|
|
7
|
+
return `Error: ${e.message}`;
|
|
8
|
+
return `Error: ${String(e)}`;
|
|
9
|
+
}
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new McpServer({ name: "catfact-mcp", version: "1.0.0" });
|
|
12
|
+
server.tool("random_cat_fact", "One random cat fact.", {
|
|
13
|
+
// no arguments
|
|
14
|
+
}, async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
return text(await randomCatFact());
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.tool("cat_breeds", "The cat breed list with country of origin, coat and pattern.", {
|
|
23
|
+
limit: z.number().int().min(1).max(50).optional().describe('Breeds per page, default 10'), page: z.number().int().min(1).max(50).optional().describe('Page number, default 1')
|
|
24
|
+
}, async (a) => {
|
|
25
|
+
try {
|
|
26
|
+
return text(await catBreeds(a));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("list_facts", "Browse the cat fact database page by page.", {
|
|
33
|
+
limit: z.number().int().min(1).max(50).optional().describe('Facts per page, default 10'), page: z.number().int().min(1).max(50).optional().describe('Page number, default 1')
|
|
34
|
+
}, async (a) => {
|
|
35
|
+
try {
|
|
36
|
+
return text(await listFacts(a));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
return server;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/catfact-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cat facts and breed data from catfact.ninja - a random fact, fact browsing by page, and the cat breed list, keyless.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/catfact-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mrfentmen/awesome-servers.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"catfact-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"cats",
|
|
25
|
+
"facts",
|
|
26
|
+
"breeds",
|
|
27
|
+
"animals",
|
|
28
|
+
"fun",
|
|
29
|
+
"keyless"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|