Is mcp-server safe to install?

Yes — mcp-server is safe to install, with something worth knowing. Fable 5.1 read every file mcp-server ships from opensubtitles/mcp.opensubtitles.com on 7 September 2026 and found nothing that acts against the person installing it; the static scan of its 26 rules grades it A.

Reviewed by Fable 5.1 on 7 September 2026 · a grade is what a scan and a reading found, not a guarantee · how this works

What the reviewer found

OpenSubtitles' own MCP server: it searches and downloads subtitles from api.opensubtitles.com over HTTPS with the vendor's built-in API key, and hashes a local video file you name. Your own key or account login can only be passed as tool arguments (user_api_key, or username and password), so it sits in the chat transcript and is written to the MCP client's stderr log; the OPENSUBTITLES_USER_KEY variable the README tells you to set is never read.

This is OpenSubtitles' own MCP server: it searches and downloads subtitles from api.opensubtitles.com over HTTPS and hashes a local video file so you can search by hash. It ships with the vendor's API key built in and uses it for every request unless you supply your own, and the only way to supply your own is as tool arguments (user_api_key, or username and password), so the credential is written by the model, kept in the chat transcript and echoed into the MCP client's stderr log together with the request headers; the OPENSUBTITLES_USER_KEY variable the README mentions is never read. The calculate_file_hash tool opens any path the model names but returns only a checksum and size, and no model-supplied value ever becomes a host or URL, so nothing in the package can send data anywhere but OpenSubtitles. The package also contains an unauthenticated HTTP mode (started if you run the command by hand in a terminal) and a remote-proxy bin that posts tool arguments over plain HTTP; the catalogued stdio config uses neither. Verdict: low risk, a legitimate vendor tool that does what it says, but do not hand it your OpenSubtitles password.

Findings

11 findings, each with the file and line it was read at: 3 medium, 4 low, 4 info.

F1mediumTool arguments and API request headers, including passwords, keys and bearer tokens, are written to stderr

dist/server.js:314,317; dist/api-client.js:120; dist/tools/search-subtitles.js:43; dist/tools/download-subtitle.js:31; dist/index.js:196

console.error("DEBUG: handleToolCall called with:", JSON.stringify(params, null, 2));  ...  console.error(`Headers:`, JSON.stringify(config.headers, null, 2));

In stdio mode stderr is captured by the MCP client and written to its log files on disk (Claude Desktop's mcp-server-*.log, Claude Code's debug log). Every tool call logs the full argument object, which is where user_api_key, username and password arrive (F2), and every outbound request logs its headers, which carry Api-Key and Authorization: Bearer <session token>. A successful login also logs the username. Nothing is redacted.

What to do:Author: strip user_api_key, username, password, Api-Key and Authorization before logging, or remove the DEBUG logging. User: never pass your account password; if you pass an API key, treat the MCP client's log directory as containing it.

F2mediumYour OpenSubtitles key or password can only be supplied as tool arguments; the env var the README documents is never read

dist/server.js:87-94,129-140; dist/tools/search-subtitles.js:22-24; dist/tools/download-subtitle.js:10-12; dist/index.js:391,477,748-752; README.md:59,73,451,504

username: { type: "string", description: "OpenSubtitles.com username for authentication" }, password: { type: "string", description: "OpenSubtitles.com password for authentication" }   // the only process.env reads in the package are MCP_MODE, MCP_TEST_MODE and PORT

README.md tells you to set OPENSUBTITLES_USER_KEY (lines 73, 451, 504) and LOG_LEVEL (line 59); no code reads either, and dotenv is a declared dependency that is never imported. So the only path for a credential is a tool call: the model writes it, it stays in the chat transcript, and it is logged (F1). The search_subtitles input schema (server.js:15-96, additionalProperties: false) exposes username and password but not user_api_key, so the documented way for the model to authenticate a search is your account password, even though the handler would accept user_api_key (search-subtitles.js:22). If you set the env var and pass nothing, the vendor's shared key (F3) is used silently.

What to do:Author: read OPENSUBTITLES_USER_KEY from the environment and prefer it over arguments; add user_api_key to the search schema and remove username/password from both tool schemas. User: if you must authenticate, pass user_api_key (a key, not your password) and accept that it appears in the transcript; never use username/password.

F6mediumremote-proxy.js (bin mcp-opensubtitles-remote, not the catalogued entrypoint) sends every tool argument over plain HTTP

remote-proxy.js:11,132-135; README.md:80-105

const REMOTE_SERVER_URL = "http://mcp.opensubtitles.com";  ...  axiosInstance.post(`${REMOTE_SERVER_URL}/proxy`, { tool: request.params.name, arguments: request.params.arguments })

This bin forwards each tool call, including user_api_key, username, password and file_path, in cleartext to the vendor's host, where anyone on the network path can read it, and the calculate_file_hash call is then executed on the vendor's server against its filesystem, not yours. The catalogued config runs dist/index.js and never loads this file, but README.md:80-105 recommends it as 'Remote Mode'.

What to do:Author: switch to https:// and carry credentials in a header, not the body. User: do not use mcp-opensubtitles-remote or the README's Remote Mode config with any credential.

F3lowA hard-coded vendor API key is sent with every request that carries no user key

dist/api-client.js:87,164,221,270

defaultApiKey = "A4grIoZ8vC7C75aE1NxShRVwbqrLMsB2";  ...  headers["Api-Key"] = this.defaultApiKey;

The package is published by the opensubtitles npm account ([email protected]), so this is the vendor's own key, not someone else's. It is shared by every installer and readable by anyone who downloads the tarball; the vendor's quota applies to all anonymous users together, and the README says anonymous downloads are 0 per day. It is also the key used for the /api/v1/login call when username/password are given.

What to do:Nothing for the user to do. Author: acceptable for a vendor-owned key, but expect it to be lifted and abused; keep it rate-limited server-side and rotate it.

F4lowcalculate_file_hash opens any path the model names, with no base directory or extension check

dist/tools/calculate-file-hash.js:13-16,24-31; dist/utils/hash-calculator.js:11,14,18,20

const resolvedPath = resolve(validatedArgs.file_path); ... await access(resolvedPath, constants.R_OK);  ...  createReadStream(filePath, { start, end: start + length - 1 })

It stats the file and reads the first and last 64KB, returning a 64-bit checksum, the byte size and the resolved absolute path, never the content. The 'too small' error (hash-calculator.js:14) confirms the existence of files under 128KB. A prompt-injected agent can therefore learn whether a file exists and how large it is, and nothing more; there is no channel that sends even that anywhere except back into the chat.

What to do:Author: restrict to a configurable media directory or to video file extensions. User: this is what a hash tool does; read the file_path in the tool call before approving it.

F5lowHTTP mode is an unauthenticated server on all interfaces, and running the command by hand in a terminal turns it on

dist/index.js:751,477,480-483,525-554,609,689,736

const mode = process.env.MCP_MODE || (process.stdin.isTTY ? 'http' : 'stdio');  ...  app.use(cors({ origin: true, credentials: true }));  ...  app.post('/proxy', ...) -> openSubtitlesServer.handleToolCall({ name: tool, arguments: args || {} })  ...  app.listen(port, ...)

Not what the catalogue entry runs: under an MCP client stdin is a pipe, so the server is stdio. But `npx -y @opensubtitles/mcp-server` typed into a terminal starts Express on 0.0.0.0:1620 (PORT) with no authentication, CORS that reflects any origin with credentials allowed, and POST /proxy that executes any tool for any caller, including calculate_file_hash with any path. That gives the LAN and any web page open in your browser a file-existence oracle and free use of your quota. GET /message and the /wp-json/wp/v2/wpmcp/streamable alias echo the caller's request headers back (index.js:609,689); HTTP mode also logs every request's params (index.js:196). The startup banner advertises /sse, /web and /tools routes (index.js:739-742) that are never registered.

What to do:Author: bind to 127.0.0.1 by default, require a token on /proxy and /message, drop credentials: true. User: add "env": {"MCP_MODE": "stdio"} to the config as the README's own example does, and never start it in a terminal on a shared network.

F7lowTwo medium vulnerabilities in qs, reachable only while the HTTP listener is running

dist/index.js:6-7; package.json:44-49

Snyk: [email protected] SNYK-JS-QS-19432017 (Allocation of Resources Without Limits or Throttling) and SNYK-JS-QS-19432019 (Uncaught Exception), 8 vulnerable paths via [email protected] and @modelcontextprotocol/[email protected] > [email protected], fixed in qs 6.16.0. npm audit: 3 moderate (qs, body-parser, express).

qs parses query strings of incoming HTTP requests. express and cors are imported unconditionally at the top of dist/index.js, so the code is loaded in stdio mode, but nothing listens there and no attacker input reaches it. In HTTP mode (F5) a crafted query string can crash or stall the server.

What to do:Author: bump qs to >= 6.16.0 and republish. User: stay in stdio mode; the vulnerable code is unreachable there.

F8infoSupply chain: vendor-published, under a year old, six versions in 18 hours then silence, no repository field, floating ranges

package.json:2-3,35-39,43-50; npm registry time and maintainers fields

created 2025-09-27T22:10:59Z; 1.4.0, 1.4.1, 1.4.2, 1.4.3, 1.4.4 and 1.5.0 all published between 2025-09-27T22:10Z and 2025-09-28T15:56Z; maintainers: opensubtitles <[email protected]>; repository, homepage and bugs fields absent.

The npm publisher matches the GitHub org (opensubtitles/mcp.opensubtitles.com, 2 stars, last push 2025-09-28), so authorship is consistent with the vendor. There is no repository field or provenance attestation to tie the tarball to a commit. No preinstall, install or postinstall scripts; prepublish runs the TypeScript build at publish time only. Every dependency is a caret range, so `npx -y` resolves fresh versions on each install: Snyk resolved @modelcontextprotocol/[email protected] against a ^1.17.5 spec. The tarball is 41 files: 36 under dist/ (9 modules as js, js.map, d.ts, d.ts.map), http-proxy.js, remote-proxy.js, LICENSE, package.json, README.md. Socket: supplyChain, quality, maintenance, vulnerability, license and overall all 1.0, 0 alerts. Our regex grade A covered only the 113-byte config and says nothing about this code.

What to do:Author: add a repository field and publish with npm provenance. User: pin @opensubtitles/[email protected] in args for a repeatable install.

F9infoSubtitle files and uploader comments are returned verbatim into the model's context

dist/tools/download-subtitle.js:55-60; dist/tools/search-subtitles.js:121-122; dist/api-client.js:255-261

const subtitleContent = await client.downloadSubtitleContent(downloadInfo.link); ... content: subtitleContent   //   if (subtitle.upload_info.comments) summaryText += `- **Notes:** ${subtitle.upload_info.comments}\n`;

download_subtitle fetches the file from the link the vendor's API returns (a URL from the HTTPS response, not from the model) and returns the whole subtitle as text; search results render each uploader's comments as Notes. Both are free text written by strangers and land in the agent's context, like any web fetch. Nothing is written to disk.

What to do:User: treat downloaded subtitles and the Notes lines as untrusted data, not instructions.

F10infoThe resource guide and error strings nudge toward the vendor's API key and premium plan; nothing steers the agent beyond the tool's purpose

dist/server.js:11,101,148,194-306 (esp. 295,304); dist/api-client.js:136

- Encourage users to get free API key from OpenSubtitles.com for better experience  ...  throw new Error("Download limit reached. Get your free API key at opensubtitles.com/api or upgrade to premium at mcp.opensubtitles.com/premium");

The single resource opensubtitles://guide/intelligent-search is a how-to-search guide with mild vendor promotion. Tool descriptions are one plain sentence each. No instruction to read other files, call other tools, contact other hosts or hide anything from the user.

What to do:None needed.

F11infoREADME and code disagree on the download tool's parameters, and two shipped helpers are dead or broken

README.md:147-148 vs dist/tools/download-subtitle.js:3-13; http-proxy.js:41; dist/simple-index.js

README documents subtitle_id (string) and format for download_subtitle; the schema requires numeric file_id and has no format. http-proxy.js does GET `${REMOTE_SERVER_URL}/tools`, a route dist/index.js never registers.

Following the README's download example fails validation. The mcp-opensubtitles-http bin cannot list tools against this server. dist/simple-index.js is a leftover debug stub (a test_tool returning a fixed string) wired to no bin. Not a security issue; it shows how much of the package is untested.

What to do:Author: fix the README, delete simple-index.js and the /tools call. User: pass file_id from search results.

What was read

The package the configuration runs, @opensubtitles/[email protected], fetched from the npm registry and read in full — its runtime code and every entrypoint it ships — together with its README, its dependency tree and what Socket and Snyk said about it. The catalogue entry itself is only the configuration that starts it.

What the static scan said

The 26-rule scan found nothing in this file; the review read it anyway.

How this review was made

Fable 5.1 read the files above on 7 September 2026 and answered three questions: is it dangerous to whoever installs it, is each scanner finding real, and what should the installer know. The verdict is bound to the file's hash; when the file changes, it is scanned afresh and reviewed again. A script that changes while the definition does not is not re-reviewed — that is a known gap. How the scan and the review work.

Produced by agentmods.dev · Fable 5.1 · 7 September 2026

← Back to mcp-server