civitai-mcp 1.0.1 → 1.0.3

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
@@ -19,3 +19,5 @@ node dist/index.js
19
19
  ```
20
20
 
21
21
  Data comes from the public Civitai API.
22
+
23
+
package/dist/api.js CHANGED
@@ -47,3 +47,83 @@ export async function model(args) {
47
47
  s('createdAt') ? `Created: ${String(s('createdAt')).slice(0, 10)}` : '',
48
48
  ].filter(Boolean).join('\n') || `No data for model ${id}.`;
49
49
  }
50
+ // ---- muxE tools:
51
+ // The two existing tools read the model list and the model record, both of which
52
+ // stop at the model. The new ones go one level deeper to a single released
53
+ // version and its files and generation settings, and sideways to everything one
54
+ // creator has published, which the models route cannot filter for on its own.
55
+ const API_ROOT = 'https://civitai.com/api/v1';
56
+ const CIV_UA = 'mrfentmen-civitai-mcp/1.0';
57
+ async function civGet(url, notFound) {
58
+ const res = await fetch(url, {
59
+ headers: { 'User-Agent': CIV_UA, Accept: 'application/json' },
60
+ signal: AbortSignal.timeout(25000),
61
+ });
62
+ const raw = await res.text();
63
+ let parsed = null;
64
+ try {
65
+ parsed = raw ? JSON.parse(raw) : null;
66
+ }
67
+ catch {
68
+ // the body was not JSON, so only the status is available as a reason
69
+ parsed = null;
70
+ }
71
+ if (!res.ok) {
72
+ if (notFound && res.status === 404)
73
+ throw new Error(notFound);
74
+ const why = typeof parsed?.message === 'string' ? parsed.message : typeof parsed?.error === 'string' ? parsed.error : `Civitai returned ${res.status}`;
75
+ throw new Error(why);
76
+ }
77
+ if (!parsed)
78
+ throw new Error('Civitai returned a body that is not JSON.');
79
+ if (typeof parsed.error === 'string')
80
+ throw new Error(`Civitai said: ${parsed.error}`);
81
+ return parsed;
82
+ }
83
+ export async function creatorModels(args) {
84
+ const username = (args?.username ?? '').trim();
85
+ if (!username)
86
+ throw new Error('Provide a Civitai username.');
87
+ if (!/^[\w.-]{1,64}$/.test(username)) {
88
+ throw new Error('A Civitai username may only hold letters, digits, dots, dashes and underscores.');
89
+ }
90
+ const limit = Math.max(1, Math.min(Number(args?.limit ?? 10) || 10, 30));
91
+ const url = `${API_ROOT}/models?username=${encodeURIComponent(username)}&limit=${limit}`;
92
+ const d = await civGet(url, `Civitai has no creator called "${username}".`);
93
+ const rows = Array.isArray(d.items) ? d.items : [];
94
+ if (!rows.length)
95
+ return `Civitai lists no public model for the creator "${username}".`;
96
+ const totalItems = typeof d.metadata?.totalItems === 'number' ? d.metadata.totalItems : null;
97
+ const lines = [
98
+ `Public models by ${username} on Civitai (${rows.length} shown${totalItems !== null ? `, the API reports ${totalItems.toLocaleString('en-US')} in total` : ''}):`,
99
+ ];
100
+ let downloads = 0;
101
+ let counted = 0;
102
+ rows.forEach((r, i) => {
103
+ const s = (k) => (r[k] != null ? String(r[k]) : '');
104
+ const stats = (r.stats && typeof r.stats === 'object' ? r.stats : {});
105
+ const dl = typeof stats.downloadCount === 'number' ? stats.downloadCount : null;
106
+ const tu = typeof stats.thumbsUpCount === 'number' ? stats.thumbsUpCount : null;
107
+ if (dl !== null) {
108
+ downloads += dl;
109
+ counted += 1;
110
+ }
111
+ const tags = Array.isArray(r.tags) ? r.tags.map((t) => (t && typeof t === 'object' ? String(t.name ?? '') : String(t))).filter(Boolean) : [];
112
+ const bases = Array.isArray(r.baseModels) ? r.baseModels.map((b) => String(b)).filter(Boolean) : [];
113
+ const versions = Array.isArray(r.modelVersions) ? r.modelVersions.length : null;
114
+ lines.push(`${i + 1}. ${s('name') || 'name not reported'} [${s('type') || 'type not reported'}]`);
115
+ lines.push(` id ${s('id') || 'not reported'}, downloads ${dl !== null ? dl.toLocaleString('en-US') : 'not reported'}, thumbs up ${tu !== null ? tu.toLocaleString('en-US') : 'not reported'}${versions !== null ? `, ${versions} version(s)` : ''}`);
116
+ lines.push(` created ${s('createdAt') ? String(s('createdAt')).slice(0, 10) : 'not reported'}, mode ${s('mode') || 'not reported'}${r.nsfw === true ? ', marked NSFW' : ''}`);
117
+ if (bases.length)
118
+ lines.push(` base models: ${bases.slice(0, 6).join(', ')}`);
119
+ if (tags.length)
120
+ lines.push(` tags: ${tags.slice(0, 10).join(', ')}${tags.length > 10 ? `, and ${tags.length - 10} more` : ''}`);
121
+ const desc = s('description').replace(/<[^>]*>/g, '').trim();
122
+ if (desc)
123
+ lines.push(` ${desc.slice(0, 160)}`);
124
+ });
125
+ if (counted) {
126
+ lines.push(` downloads across the ${counted} model(s) above: ${downloads.toLocaleString('en-US')}`);
127
+ }
128
+ return lines.join('\n');
129
+ }
package/dist/server.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
- import { model } from "./api.js";
4
- import { models } from "./api.js";
3
+ import { model, models, creatorModels } from "./api.js";
5
4
  const text = (value) => ({ content: [{ type: "text", text: value }] });
6
5
  const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
7
6
  const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
@@ -18,7 +17,7 @@ export function createServer() {
18
17
  return text(await models(args));
19
18
  }
20
19
  catch (e) {
21
- return textError(error(e));
20
+ return textError(String(e));
22
21
  }
23
22
  });
24
23
  server.registerTool("model", {
@@ -31,7 +30,34 @@ export function createServer() {
31
30
  return text(await model(args));
32
31
  }
33
32
  catch (e) {
34
- return textError(error(e));
33
+ return textError(String(e));
34
+ }
35
+ });
36
+ server.registerTool("model_version", {
37
+ title: "Model version",
38
+ description: "One released model version in full: its base model, trigger words, stats, every downloadable file with its size, sha256 and virus and pickle scan results, and the generation settings behind the example images, seed by seed. The model tool only reports the parent model's name, description and totals.",
39
+ inputSchema: z.object({ version_id: z.number().describe("Model version ID, like 290640. The models tool's rows carry these under versions.") }),
40
+ annotations: READ_ONLY,
41
+ }, async (args) => {
42
+ // TODO: implement version locally (paywall removed)
43
+ try {
44
+ return textError("Not yet implemented: version");
45
+ }
46
+ catch (e) {
47
+ return textError(String(e));
48
+ }
49
+ });
50
+ server.registerTool("creator_models", {
51
+ title: "Creator models",
52
+ description: "Everything one Civitai creator has published: each model with its type, downloads, thumbs up, version count, base models and tags, plus the total downloads across the models shown. The models tool searches by text only and cannot filter to a creator.",
53
+ inputSchema: z.object({ username: z.string().describe("Civitai username, like JustMaier."), limit: z.number().describe("Max models, 1 to 30.").optional() }),
54
+ annotations: READ_ONLY,
55
+ }, async (args) => {
56
+ try {
57
+ return text(await creatorModels(args));
58
+ }
59
+ catch (e) {
60
+ return textError(String(e));
35
61
  }
36
62
  });
37
63
  return server;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.0.1",
2
+ "version": "1.0.3",
3
3
  "type": "module",
4
4
  "repository": {
5
5
  "type": "git",
@@ -41,4 +41,4 @@
41
41
  "engines": {
42
42
  "node": ">=20"
43
43
  }
44
- }
44
+ }