demozoo-mcp 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 ADDED
@@ -0,0 +1,23 @@
1
+ # Demozoo MCP
2
+
3
+ Keyless MCP server for Demozoo, the demoscene database: search 390k+ demos, intros, graphics and music, get download links, credits, groups and sceners.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npm install
9
+ npm run build
10
+ node dist/index.js
11
+ ```
12
+
13
+ The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
14
+
15
+ ## Tools at a glance
16
+
17
+ - `search_productions`: Search demos, intros, graphics, music by text query.
18
+ - `get_production`: Full detail for one production (credits, download links, platforms).
19
+ - `get_releaser`: Group or scener profile (nicks, memberships, members).
20
+
21
+ ## Limits and privacy
22
+
23
+ This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. No credentials are required.
package/dist/api.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ export declare class DemozooError extends Error {
2
+ }
3
+ export interface ProductionSummary {
4
+ id: number;
5
+ title: string;
6
+ authors: string[];
7
+ release_date?: string;
8
+ supertype?: string;
9
+ platforms: string[];
10
+ types: string[];
11
+ demozoo_url?: string;
12
+ }
13
+ export interface ProductionDetails extends ProductionSummary {
14
+ credits: string[];
15
+ download_links: string[];
16
+ }
17
+ export interface Releaser {
18
+ id: number;
19
+ name: string;
20
+ is_group: boolean;
21
+ nicks: string[];
22
+ member_of: string[];
23
+ members: string[];
24
+ demozoo_url?: string;
25
+ }
26
+ type Raw = Record<string, any>;
27
+ export declare function toSummary(p: Raw): ProductionSummary;
28
+ export declare function searchProductions(query: string, limit?: number): Promise<ProductionSummary[]>;
29
+ export declare function getProduction(id: string): Promise<ProductionDetails | null>;
30
+ export declare function getReleaser(id: string): Promise<Releaser | null>;
31
+ export declare function formatSummary(p: ProductionSummary, index?: number): string;
32
+ export declare function formatProduction(p: ProductionDetails): string;
33
+ export declare function formatReleaser(r: Releaser): string;
34
+ export {};
package/dist/api.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Demozoo API v1 client, keyless.
3
+ * Docs: https://demozoo.org/api/v1/ (browse the root URL for the endpoint list)
4
+ */
5
+ const BASE = "https://demozoo.org/api/v1";
6
+ export class DemozooError extends Error {
7
+ }
8
+ async function getJson(path) {
9
+ const res = await fetch(`${BASE}${path}`, {
10
+ headers: { "User-Agent": "demozoo-mcp/1.0" },
11
+ signal: AbortSignal.timeout(15000),
12
+ });
13
+ if (!res.ok)
14
+ throw new DemozooError(`Demozoo error ${res.status}`);
15
+ return (await res.json());
16
+ }
17
+ const names = (arr) => Array.isArray(arr) ? arr.map((x) => String(x?.name ?? x)) : [];
18
+ export function toSummary(p) {
19
+ return {
20
+ id: p.id,
21
+ title: String(p.title ?? "(untitled)"),
22
+ authors: Array.isArray(p.author_nicks) ? p.author_nicks.map((n) => n.name) : [],
23
+ release_date: p.release_date,
24
+ supertype: p.supertype,
25
+ platforms: names(p.platforms),
26
+ types: names(p.types),
27
+ demozoo_url: p.demozoo_url,
28
+ };
29
+ }
30
+ export async function searchProductions(query, limit = 5) {
31
+ const data = await getJson(`/productions/?query=${encodeURIComponent(query)}`);
32
+ return data.results.slice(0, limit).map(toSummary);
33
+ }
34
+ export async function getProduction(id) {
35
+ if (!/^\d+$/.test(id.trim()))
36
+ throw new DemozooError(`Production id must be numeric, got "${id}".`);
37
+ const p = await getJson(`/productions/${id.trim()}/`);
38
+ if (!p || p.id === undefined)
39
+ return null;
40
+ const s = toSummary(p);
41
+ return {
42
+ ...s,
43
+ credits: Array.isArray(p.credits)
44
+ ? p.credits.map((c) => `${c?.nick?.name ?? "?"} — ${c?.category ?? "?"}`).slice(0, 12)
45
+ : [],
46
+ download_links: Array.isArray(p.download_links)
47
+ ? p.download_links.map((d) => String(d.url)).slice(0, 8)
48
+ : [],
49
+ };
50
+ }
51
+ export async function getReleaser(id) {
52
+ if (!/^\d+$/.test(id.trim()))
53
+ throw new DemozooError(`Releaser id must be numeric, got "${id}".`);
54
+ const r = await getJson(`/releasers/${id.trim()}/`);
55
+ if (!r || r.id === undefined)
56
+ return null;
57
+ return {
58
+ id: r.id,
59
+ name: String(r.name ?? "?"),
60
+ is_group: Boolean(r.is_group),
61
+ nicks: Array.isArray(r.nicks) ? r.nicks.map((n) => String(n.name)) : [],
62
+ member_of: Array.isArray(r.member_of) ? r.member_of.map((m) => String(m?.group?.name ?? "?")) : [],
63
+ members: Array.isArray(r.members) ? r.members.map((m) => String(m?.nick?.name ?? "?")).slice(0, 15) : [],
64
+ demozoo_url: r.demozoo_url,
65
+ };
66
+ }
67
+ export function formatSummary(p, index) {
68
+ const prefix = index !== undefined ? `${index + 1}. ` : "";
69
+ const by = p.authors.length ? ` by ${p.authors.slice(0, 3).join(", ")}` : "";
70
+ const meta = [p.release_date, p.platforms.slice(0, 3).join("/"), p.types.slice(0, 2).join(", ")]
71
+ .filter(Boolean)
72
+ .join(" · ");
73
+ return `${prefix}[${p.id}] ${p.title}${by}${meta ? ` (${meta})` : ""}`;
74
+ }
75
+ export function formatProduction(p) {
76
+ const lines = [
77
+ `[${p.id}] ${p.title}`,
78
+ p.authors.length ? `By: ${p.authors.join(", ")}` : "",
79
+ p.release_date ? `Released: ${p.release_date}` : "",
80
+ p.platforms.length ? `Platforms: ${p.platforms.join(", ")}` : "",
81
+ p.types.length ? `Type: ${p.types.join(", ")}` : "",
82
+ p.credits.length ? `Credits:\n- ${p.credits.join("\n- ")}` : "",
83
+ p.download_links.length ? `Downloads:\n- ${p.download_links.join("\n- ")}` : "",
84
+ p.demozoo_url ? `More: ${p.demozoo_url}` : "",
85
+ ].filter(Boolean);
86
+ return lines.join("\n");
87
+ }
88
+ export function formatReleaser(r) {
89
+ const lines = [
90
+ `[${r.id}] ${r.name} (${r.is_group ? "group" : "scener"})`,
91
+ r.nicks.length > 1 ? `Nicks: ${r.nicks.join(", ")}` : "",
92
+ r.member_of.length ? `Member of: ${r.member_of.join(", ")}` : "",
93
+ r.members.length ? `Members: ${r.members.join(", ")}` : "",
94
+ r.demozoo_url ? `More: ${r.demozoo_url}` : "",
95
+ ].filter(Boolean);
96
+ return lines.join("\n");
97
+ }
@@ -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("MCP server running on stdio");
8
+ }
9
+ main().catch((err) => {
10
+ console.error("Fatal error:", err);
11
+ process.exit(1);
12
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,75 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { DemozooError, formatProduction, formatReleaser, formatSummary, getProduction, getReleaser, searchProductions, } from "./api.js";
4
+ const text = (t) => ({ content: [{ type: "text", text: t }] });
5
+ const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
6
+ const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
7
+ export function createServer() {
8
+ const server = new McpServer({
9
+ name: "demozoo-mcp",
10
+ version: "1.0.0",
11
+ });
12
+ server.registerTool("search_productions", {
13
+ title: "Search demoscene productions",
14
+ description: "Search Demozoo for demoscene productions: demos, intros, graphics, music. Finds titles across 390k+ entries.",
15
+ inputSchema: z.object({
16
+ query: z.string().describe("Search text, e.g. 'second reality', 'farbrausch', 'amiga cracktro'"),
17
+ limit: z.number().int().min(1).max(20).default(5),
18
+ }),
19
+ annotations: READ_ONLY,
20
+ }, async ({ query, limit }) => {
21
+ try {
22
+ const results = await searchProductions(query, limit);
23
+ if (results.length === 0)
24
+ return text(`No Demozoo productions match "${query}".`);
25
+ return text(`Demozoo results for "${query}":\n\n${results.map((p, i) => formatSummary(p, i)).join("\n")}`);
26
+ }
27
+ catch (e) {
28
+ return textError(errorMessage(e));
29
+ }
30
+ });
31
+ server.registerTool("get_production", {
32
+ title: "Get production details",
33
+ description: "Get a Demozoo production by id: authors, release date, platforms, credits, and download links.",
34
+ inputSchema: z.object({
35
+ id: z.string().describe("Numeric Demozoo production id, e.g. '1' (use search_productions to find it)"),
36
+ }),
37
+ annotations: READ_ONLY,
38
+ }, async ({ id }) => {
39
+ try {
40
+ const p = await getProduction(id);
41
+ if (!p)
42
+ return text(`No Demozoo production with id ${id}.`);
43
+ return text(formatProduction(p));
44
+ }
45
+ catch (e) {
46
+ return textError(errorMessage(e));
47
+ }
48
+ });
49
+ server.registerTool("get_releaser", {
50
+ title: "Get group or scener profile",
51
+ description: "Get a Demozoo group or scener profile by id: nicks, group memberships, members.",
52
+ inputSchema: z.object({
53
+ id: z.string().describe("Numeric Demozoo releaser id, e.g. '16685' (shown in production credits)"),
54
+ }),
55
+ annotations: READ_ONLY,
56
+ }, async ({ id }) => {
57
+ try {
58
+ const r = await getReleaser(id);
59
+ if (!r)
60
+ return text(`No Demozoo group/scener with id ${id}.`);
61
+ return text(formatReleaser(r));
62
+ }
63
+ catch (e) {
64
+ return textError(errorMessage(e));
65
+ }
66
+ });
67
+ return server;
68
+ }
69
+ function errorMessage(e) {
70
+ if (e instanceof DemozooError)
71
+ return `Error: ${e.message}`;
72
+ if (e instanceof Error)
73
+ return `Error: ${e.message}`;
74
+ return `Error: ${String(e)}`;
75
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "demozoo-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Keyless MCP server for Demozoo, the demoscene database: search 390k+ demos, intros, graphics and music, get download links, credits, groups and sceners.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/demozoo-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/mrfentmen/awesome-mcps.git"
10
+ },
11
+ "bin": {
12
+ "demozoo-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
+ "dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
22
+ "inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "demozoo",
27
+ "demoscene",
28
+ "api"
29
+ ],
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "@modelcontextprotocol/sdk": "^1.0.4",
33
+ "zod": "^3.23.8"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.0.0",
37
+ "typescript": "^5.6.0"
38
+ },
39
+ "engines": {
40
+ "node": ">=20"
41
+ }
42
+ }