node-csfd-api 4.1.6 → 4.2.0-next.1

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/mcp-server.mjs +192 -0
  2. package/package.json +14 -5
  3. package/server.mjs +324 -0
package/mcp-server.mjs ADDED
@@ -0,0 +1,192 @@
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
+ import packageJson from "./package.json" with { type: "json" };
6
+ import { csfd } from "./index.mjs";
7
+ const server = new McpServer({
8
+ name: packageJson.name,
9
+ version: packageJson.version
10
+ });
11
+ server.tool(
12
+ "search",
13
+ "Searches for a movie, TV series, or person on CSFD.cz. Returns a list of results with IDs. Use this tool FIRST to find the ID needed for other tools.",
14
+ {
15
+ query: z.string().describe("Search query (movie title, series, or actor name)")
16
+ },
17
+ async ({ query }) => {
18
+ try {
19
+ const results = await csfd.search(query);
20
+ return {
21
+ content: [
22
+ {
23
+ type: "text",
24
+ text: JSON.stringify(results, null, 2)
25
+ }
26
+ ]
27
+ };
28
+ } catch (error) {
29
+ return {
30
+ content: [{ type: "text", text: `Error during search: ${error}` }],
31
+ isError: true
32
+ };
33
+ }
34
+ }
35
+ );
36
+ server.tool(
37
+ "get_movie",
38
+ "Retrieves detailed information about a specific movie or series, including rating, plot, genres, and actors. Requires a numeric CSFD ID.",
39
+ {
40
+ id: z.number().describe("CSFD Movie ID (found using the 'search' tool)")
41
+ },
42
+ async ({ id }) => {
43
+ try {
44
+ const movie = await csfd.movie(id);
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text",
49
+ text: JSON.stringify(movie, null, 2)
50
+ }
51
+ ]
52
+ };
53
+ } catch (error) {
54
+ return {
55
+ content: [{ type: "text", text: `Error retrieving movie details: ${error}` }],
56
+ isError: true
57
+ };
58
+ }
59
+ }
60
+ );
61
+ server.tool(
62
+ "get_creator",
63
+ "Retrieves information about a specific creator (actor, director, etc.), including their biography and filmography. Requires a numeric CSFD ID.",
64
+ {
65
+ id: z.number().describe("CSFD Creator ID (found using the 'search' tool)")
66
+ },
67
+ async ({ id }) => {
68
+ try {
69
+ const creator = await csfd.creator(id);
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: JSON.stringify(creator, null, 2)
75
+ }
76
+ ]
77
+ };
78
+ } catch (error) {
79
+ return {
80
+ content: [{ type: "text", text: `Error retrieving creator details: ${error}` }],
81
+ isError: true
82
+ };
83
+ }
84
+ }
85
+ );
86
+ server.tool(
87
+ "get_user_ratings",
88
+ "Retrieves movie ratings from a specific CSFD user. Returns a list of movies with their user rating (0-5 stars). Supports pagination and filtering by film type.",
89
+ {
90
+ user: z.union([z.string(), z.number()]).describe("CSFD User ID (numeric) or username"),
91
+ page: z.number().optional().describe("Page number to fetch (default: 1)"),
92
+ allPages: z.boolean().optional().describe("Fetch all pages at once (use wisely, may be slow)"),
93
+ allPagesDelay: z.number().optional().describe("Delay in ms between page requests when using allPages"),
94
+ excludes: z.array(z.string()).optional().describe('Film types to exclude (e.g. "seri\xE1l", "TV film")'),
95
+ includesOnly: z.array(z.string()).optional().describe('Only include these film types (e.g. "film")')
96
+ },
97
+ async ({ user, page, allPages, allPagesDelay, excludes, includesOnly }) => {
98
+ try {
99
+ const results = await csfd.userRatings(user, {
100
+ page,
101
+ allPages,
102
+ allPagesDelay,
103
+ excludes,
104
+ includesOnly
105
+ });
106
+ return {
107
+ content: [
108
+ {
109
+ type: "text",
110
+ text: JSON.stringify(results, null, 2)
111
+ }
112
+ ]
113
+ };
114
+ } catch (error) {
115
+ return {
116
+ content: [{ type: "text", text: `Error retrieving user ratings: ${error}` }],
117
+ isError: true
118
+ };
119
+ }
120
+ }
121
+ );
122
+ server.tool(
123
+ "get_user_reviews",
124
+ "Retrieves movie reviews written by a specific CSFD user. Returns a list of movies with their review text and rating. Supports pagination and filtering by film type.",
125
+ {
126
+ user: z.union([z.string(), z.number()]).describe("CSFD User ID (numeric) or username"),
127
+ page: z.number().optional().describe("Page number to fetch (default: 1)"),
128
+ allPages: z.boolean().optional().describe("Fetch all pages at once (use wisely, may be slow)"),
129
+ allPagesDelay: z.number().optional().describe("Delay in ms between page requests when using allPages"),
130
+ excludes: z.array(z.string()).optional().describe('Film types to exclude (e.g. "seri\xE1l", "TV film")'),
131
+ includesOnly: z.array(z.string()).optional().describe('Only include these film types (e.g. "film")')
132
+ },
133
+ async ({ user, page, allPages, allPagesDelay, excludes, includesOnly }) => {
134
+ try {
135
+ const results = await csfd.userReviews(user, {
136
+ page,
137
+ allPages,
138
+ allPagesDelay,
139
+ excludes,
140
+ includesOnly
141
+ });
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: JSON.stringify(results, null, 2)
147
+ }
148
+ ]
149
+ };
150
+ } catch (error) {
151
+ return {
152
+ content: [{ type: "text", text: `Error retrieving user reviews: ${error}` }],
153
+ isError: true
154
+ };
155
+ }
156
+ }
157
+ );
158
+ server.tool(
159
+ "get_cinemas",
160
+ "Retrieves cinema screenings for a given district in Czech Republic. Returns a list of cinemas with their current screenings, showtimes, and movie details.",
161
+ {
162
+ district: z.union([z.number(), z.string()]).describe("District ID (numeric) or name"),
163
+ period: z.enum(["today", "tomorrow", "weekend", "week", "month"]).describe("Time period for screenings")
164
+ },
165
+ async ({ district, period }) => {
166
+ try {
167
+ const results = await csfd.cinema(district, period);
168
+ return {
169
+ content: [
170
+ {
171
+ type: "text",
172
+ text: JSON.stringify(results, null, 2)
173
+ }
174
+ ]
175
+ };
176
+ } catch (error) {
177
+ return {
178
+ content: [{ type: "text", text: `Error retrieving cinema data: ${error}` }],
179
+ isError: true
180
+ };
181
+ }
182
+ }
183
+ );
184
+ async function main() {
185
+ const transport = new StdioServerTransport();
186
+ await server.connect(transport);
187
+ console.error("CSFD MCP Server running on stdio...");
188
+ }
189
+ main().catch((error) => {
190
+ console.error("Fatal error in MCP server:", error);
191
+ process.exit(1);
192
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-csfd-api",
3
- "version": "4.1.6",
3
+ "version": "4.2.0-next.1",
4
4
  "description": "ČSFD API in JavaScript. Amazing NPM library for scrapping csfd.cz :)",
5
5
  "author": "BART! <bart@bartweb.cz>",
6
6
  "publishConfig": {
@@ -8,8 +8,12 @@
8
8
  "registry": "https://registry.npmjs.org"
9
9
  },
10
10
  "dependencies": {
11
+ "@modelcontextprotocol/sdk": "^1.26.0",
11
12
  "cross-fetch": "^4.1.0",
12
- "node-html-parser": "^7.0.2"
13
+ "node-html-parser": "^7.0.2",
14
+ "express": "^5.2.1",
15
+ "dotenv": "^17.2.4",
16
+ "zod": "^4.3.6"
13
17
  },
14
18
  "repository": {
15
19
  "url": "git+https://github.com/bartholomej/node-csfd-api.git",
@@ -41,9 +45,14 @@
41
45
  "types": "./index.d.ts",
42
46
  "exports": {
43
47
  ".": {
44
- "require": "./index.js",
45
- "import": "./index.mjs"
48
+ "import": "./index.mjs",
49
+ "require": "./index.js"
46
50
  },
47
51
  "./package.json": "./package.json"
48
- }
52
+ },
53
+ "bin": {
54
+ "csfd-mcp": "mcp-server.mjs",
55
+ "csfd-server": "server.mjs"
56
+ },
57
+ "sideEffects": false
49
58
  }
package/server.mjs ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ import express from "express";
4
+ import packageJson from "./package.json" with { type: "json" };
5
+ import { csfd } from "./index.mjs";
6
+ const LOG_COLORS = {
7
+ info: "\x1B[36m",
8
+ // cyan
9
+ warn: "\x1B[33m",
10
+ // yellow
11
+ error: "\x1B[31m",
12
+ // red
13
+ success: "\x1B[32m",
14
+ // green
15
+ reset: "\x1B[0m"
16
+ };
17
+ const LOG_SYMBOLS = {
18
+ info: "\u2139\uFE0F",
19
+ warn: "\u26A0\uFE0F",
20
+ error: "\u274C",
21
+ success: "\u2705"
22
+ };
23
+ const LOG_PADDED_SEVERITY = {
24
+ info: "INFO ",
25
+ warn: "WARN ",
26
+ error: "ERROR ",
27
+ success: "SUCCESS"
28
+ };
29
+ var Errors = /* @__PURE__ */ ((Errors2) => {
30
+ Errors2["API_KEY_MISSING"] = "API_KEY_MISSING";
31
+ Errors2["API_KEY_INVALID"] = "API_KEY_INVALID";
32
+ Errors2["ID_MISSING"] = "ID_MISSING";
33
+ Errors2["MOVIE_FETCH_FAILED"] = "MOVIE_FETCH_FAILED";
34
+ Errors2["CREATOR_FETCH_FAILED"] = "CREATOR_FETCH_FAILED";
35
+ Errors2["SEARCH_FETCH_FAILED"] = "SEARCH_FETCH_FAILED";
36
+ Errors2["USER_RATINGS_FETCH_FAILED"] = "USER_RATINGS_FETCH_FAILED";
37
+ Errors2["USER_REVIEWS_FETCH_FAILED"] = "USER_REVIEWS_FETCH_FAILED";
38
+ Errors2["CINEMAS_FETCH_FAILED"] = "CINEMAS_FETCH_FAILED";
39
+ Errors2["PAGE_NOT_FOUND"] = "PAGE_NOT_FOUND";
40
+ Errors2["TOO_MANY_REQUESTS"] = "TOO_MANY_REQUESTS";
41
+ return Errors2;
42
+ })(Errors || {});
43
+ function logMessage(severity, log, req) {
44
+ const time = (/* @__PURE__ */ new Date()).toISOString();
45
+ const reqInfo = req ? `${req.method}: ${req.originalUrl}` : "";
46
+ const reqIp = req ? req.headers["x-forwarded-for"] || req.socket.remoteAddress || req.ip || req.ips : "";
47
+ const msg = `${LOG_COLORS[severity]}[${LOG_PADDED_SEVERITY[severity]}]${LOG_COLORS.reset} ${time} | IP: ${reqIp} ${LOG_SYMBOLS[severity]} ${log.error ? log.error + ":" : ""} ${log.message} \u{1F517} ${reqInfo}`;
48
+ const logSuccessEnabled = process.env.VERBOSE === "true";
49
+ if (severity === "success") {
50
+ if (logSuccessEnabled) {
51
+ console.log(msg);
52
+ }
53
+ } else if (severity === "error") {
54
+ console.error(msg);
55
+ } else if (severity === "warn") {
56
+ console.warn(msg);
57
+ } else {
58
+ console.log(msg);
59
+ }
60
+ }
61
+ var Endpoint = /* @__PURE__ */ ((Endpoint2) => {
62
+ Endpoint2["MOVIE"] = "/movie/:id";
63
+ Endpoint2["CREATOR"] = "/creator/:id";
64
+ Endpoint2["SEARCH"] = "/search/:query";
65
+ Endpoint2["USER_RATINGS"] = "/user-ratings/:id";
66
+ Endpoint2["USER_REVIEWS"] = "/user-reviews/:id";
67
+ Endpoint2["CINEMAS"] = "/cinemas";
68
+ return Endpoint2;
69
+ })(Endpoint || {});
70
+ const app = express();
71
+ const port = process.env.PORT || 3e3;
72
+ const API_KEY_NAME = process.env.API_KEY_NAME || "x-api-key";
73
+ const API_KEY = process.env.API_KEY;
74
+ const RAW_LANGUAGE = process.env.LANGUAGE;
75
+ const isSupportedLanguage = (value) => value === "cs" || value === "en" || value === "sk";
76
+ const BASE_LANGUAGE = isSupportedLanguage(RAW_LANGUAGE) ? RAW_LANGUAGE : void 0;
77
+ const API_KEYS_LIST = API_KEY ? API_KEY.split(/[,;\s]+/).map((k) => k.trim()).filter(Boolean) : [];
78
+ if (BASE_LANGUAGE) {
79
+ csfd.setOptions({ language: BASE_LANGUAGE });
80
+ }
81
+ app.use((req, res, next) => {
82
+ if (API_KEY) {
83
+ const apiKey = req.get(API_KEY_NAME)?.trim();
84
+ if (!apiKey) {
85
+ const log = {
86
+ error: "API_KEY_MISSING" /* API_KEY_MISSING */,
87
+ message: `Missing API key in request header: ${API_KEY_NAME}`
88
+ };
89
+ logMessage("error", log, req);
90
+ res.status(401).json(log);
91
+ return;
92
+ }
93
+ if (!API_KEYS_LIST.includes(apiKey)) {
94
+ const log = {
95
+ error: "API_KEY_INVALID" /* API_KEY_INVALID */,
96
+ message: `Invalid API key in request header: ${API_KEY_NAME}`
97
+ };
98
+ logMessage("error", log, req);
99
+ res.status(401).json(log);
100
+ return;
101
+ }
102
+ }
103
+ next();
104
+ });
105
+ app.get("/", (_, res) => {
106
+ logMessage("info", { error: null, message: "/" });
107
+ res.json({
108
+ name: packageJson.name,
109
+ version: packageJson.version,
110
+ docs: packageJson.homepage,
111
+ links: Object.values(Endpoint)
112
+ });
113
+ });
114
+ app.get(["/movie/", "/creator/", "/search/", "/user-ratings/", "/user-reviews/"], (req, res) => {
115
+ const log = {
116
+ error: "ID_MISSING" /* ID_MISSING */,
117
+ message: `ID is missing. Provide ID like this: ${req.url}${req.url.endsWith("/") ? "" : "/"}1234`
118
+ };
119
+ logMessage("warn", log, req);
120
+ res.status(404).json(log);
121
+ });
122
+ app.get("/movie/:id" /* MOVIE */, async (req, res) => {
123
+ const rawLanguage = req.query.language;
124
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
125
+ try {
126
+ const movie = await csfd.movie(+req.params.id, { language });
127
+ res.json(movie);
128
+ logMessage(
129
+ "success",
130
+ {
131
+ error: null,
132
+ message: `${"/movie/:id" /* MOVIE */}: ${req.params.id}${language ? ` [${language}]` : ""}`
133
+ },
134
+ req
135
+ );
136
+ } catch (error) {
137
+ const log = {
138
+ error: "MOVIE_FETCH_FAILED" /* MOVIE_FETCH_FAILED */,
139
+ message: "Failed to fetch movie data: " + error
140
+ };
141
+ logMessage("error", log, req);
142
+ res.status(500).json(log);
143
+ }
144
+ });
145
+ app.get("/creator/:id" /* CREATOR */, async (req, res) => {
146
+ const rawLanguage = req.query.language;
147
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
148
+ try {
149
+ const result = await csfd.creator(+req.params.id, { language });
150
+ res.json(result);
151
+ logMessage(
152
+ "success",
153
+ {
154
+ error: null,
155
+ message: `${"/creator/:id" /* CREATOR */}: ${req.params.id}${language ? ` [${language}]` : ""}`
156
+ },
157
+ req
158
+ );
159
+ } catch (error) {
160
+ const log = {
161
+ error: "CREATOR_FETCH_FAILED" /* CREATOR_FETCH_FAILED */,
162
+ message: "Failed to fetch creator data: " + error
163
+ };
164
+ logMessage("error", log, req);
165
+ res.status(500).json(log);
166
+ }
167
+ });
168
+ app.get("/search/:query" /* SEARCH */, async (req, res) => {
169
+ const rawLanguage = req.query.language;
170
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
171
+ try {
172
+ const result = await csfd.search(req.params.query, { language });
173
+ res.json(result);
174
+ logMessage(
175
+ "success",
176
+ {
177
+ error: null,
178
+ message: `${"/search/:query" /* SEARCH */}: ${req.params.query}${language ? ` [${language}]` : ""}`
179
+ },
180
+ req
181
+ );
182
+ } catch (error) {
183
+ const log = {
184
+ error: "SEARCH_FETCH_FAILED" /* SEARCH_FETCH_FAILED */,
185
+ message: "Failed to fetch search data: " + error
186
+ };
187
+ logMessage("error", log, req);
188
+ res.status(500).json(log);
189
+ }
190
+ });
191
+ app.get("/user-ratings/:id" /* USER_RATINGS */, async (req, res) => {
192
+ const { allPages, allPagesDelay, excludes, includesOnly, page } = req.query;
193
+ const rawLanguage = req.query.language;
194
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
195
+ try {
196
+ const result = await csfd.userRatings(
197
+ req.params.id,
198
+ {
199
+ allPages: allPages === "true",
200
+ allPagesDelay: allPagesDelay ? +allPagesDelay : void 0,
201
+ excludes: excludes ? excludes.split(",") : void 0,
202
+ includesOnly: includesOnly ? includesOnly.split(",") : void 0,
203
+ page: page ? +page : void 0
204
+ },
205
+ {
206
+ language
207
+ }
208
+ );
209
+ res.json(result);
210
+ logMessage(
211
+ "success",
212
+ {
213
+ error: null,
214
+ message: `${"/user-ratings/:id" /* USER_RATINGS */}: ${req.params.id}${language ? ` [${language}]` : ""}`
215
+ },
216
+ req
217
+ );
218
+ } catch (error) {
219
+ const log = {
220
+ error: "USER_RATINGS_FETCH_FAILED" /* USER_RATINGS_FETCH_FAILED */,
221
+ message: "Failed to fetch user-ratings data: " + error
222
+ };
223
+ logMessage("error", log, req);
224
+ res.status(500).json(log);
225
+ }
226
+ });
227
+ app.get("/user-reviews/:id" /* USER_REVIEWS */, async (req, res) => {
228
+ const { allPages, allPagesDelay, excludes, includesOnly, page } = req.query;
229
+ const rawLanguage = req.query.language;
230
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
231
+ try {
232
+ const result = await csfd.userReviews(
233
+ req.params.id,
234
+ {
235
+ allPages: allPages === "true",
236
+ allPagesDelay: allPagesDelay ? +allPagesDelay : void 0,
237
+ excludes: excludes ? excludes.split(",") : void 0,
238
+ includesOnly: includesOnly ? includesOnly.split(",") : void 0,
239
+ page: page ? +page : void 0
240
+ },
241
+ {
242
+ language
243
+ }
244
+ );
245
+ res.json(result);
246
+ logMessage(
247
+ "success",
248
+ {
249
+ error: null,
250
+ message: `${"/user-reviews/:id" /* USER_REVIEWS */}: ${req.params.id}${language ? ` [${language}]` : ""}`
251
+ },
252
+ req
253
+ );
254
+ } catch (error) {
255
+ const log = {
256
+ error: "USER_REVIEWS_FETCH_FAILED" /* USER_REVIEWS_FETCH_FAILED */,
257
+ message: "Failed to fetch user-reviews data: " + error
258
+ };
259
+ logMessage("error", log, req);
260
+ res.status(500).json(log);
261
+ }
262
+ });
263
+ app.get("/cinemas" /* CINEMAS */, async (req, res) => {
264
+ const rawLanguage = req.query.language;
265
+ const language = isSupportedLanguage(rawLanguage) ? rawLanguage : void 0;
266
+ try {
267
+ const result = await csfd.cinema(1, "today", { language });
268
+ logMessage(
269
+ "success",
270
+ { error: null, message: `${"/cinemas" /* CINEMAS */}${language ? ` [${language}]` : ""}` },
271
+ req
272
+ );
273
+ res.json(result);
274
+ } catch (error) {
275
+ const log = {
276
+ error: "CINEMAS_FETCH_FAILED" /* CINEMAS_FETCH_FAILED */,
277
+ message: "Failed to fetch cinemas data: " + error
278
+ };
279
+ logMessage("error", log, req);
280
+ res.status(500).json(log);
281
+ }
282
+ });
283
+ app.use((req, res) => {
284
+ const log = {
285
+ error: "PAGE_NOT_FOUND" /* PAGE_NOT_FOUND */,
286
+ message: "The requested endpoint could not be found."
287
+ };
288
+ logMessage("warn", log, req);
289
+ res.status(404).json(log);
290
+ });
291
+ app.listen(port, () => {
292
+ console.log(`
293
+ _ __ _ _
294
+ | | / _| | | (_)
295
+ _ __ ___ __| | ___ ___ ___| |_ __| | __ _ _ __ _
296
+ | '_ \\ / _ \\ / _\` |/ _ \\ / __/ __| _/ _\` | / _\` | '_ \\| |
297
+ | | | | (_) | (_| | __/ | (__\\__ \\ || (_| | | (_| | |_) | |
298
+ |_| |_|\\___/ \\__,_|\\___| \\___|___/_| \\__,_| \\__,_| .__/|_|
299
+ | |
300
+ |_|
301
+ `);
302
+ console.log(`node-csfd-api@${packageJson.version}
303
+ `);
304
+ console.log(`Docs: ${packageJson.homepage}`);
305
+ console.log(`Endpoints: ${Object.values(Endpoint).join(", ")}
306
+ `);
307
+ console.log(`API is running on: http://localhost:${port}`);
308
+ if (BASE_LANGUAGE) {
309
+ console.log(`Base language configured: ${BASE_LANGUAGE}
310
+ `);
311
+ }
312
+ if (API_KEYS_LIST.length === 0) {
313
+ console.log(
314
+ "\x1B[31m%s\x1B[0m",
315
+ "\u26A0\uFE0F Server is OPEN!\n- Your server will be open to the world and potentially everyone can use it without any restriction.\n- To enable some basic protection, set API_KEY environment variable (single value or comma-separated list) and provide the same value in request header: " + API_KEY_NAME
316
+ );
317
+ } else {
318
+ console.log(
319
+ "\x1B[32m%s\x1B[0m",
320
+ `\u2714\uFE0F Server is protected (somehow).
321
+ - ${API_KEYS_LIST.length} API key(s) are configured and will be checked for each request header: ${API_KEY_NAME}`
322
+ );
323
+ }
324
+ });