Notes 24
Capabilities, vulnerabilities and disclosed behavior worth knowing. Nothing here works against the person who installs it, so notes never change the verdict.
Data exfiltration
highNoteConfirmed on reviewmedium confidenceOn each check the server sends a plain GET to the publisher's own npm registry for its package version; no user data is sent, so this is a disclosed core update check.
const res = await fetch(
vector:code
Reviewer's reasoning
A plain GET to the official npm registry for the publisher's own package version sends no user data and is a disclosed core update check (tests 2 and 5).
Persistence
highNoteConfirmed on reviewmedium confidenceThe update check writes an undeclared cache file in the user's home directory (~/.search-console-mcp-update-cache.json) that the declared read-only setup does not mention; it is the artifact's own state file and only stores a timestamp and version.
CACHE_PATH, JSON.stringify({ lastCheck: now, latestVersion })vector:code
Reviewer's reasoning
The cache file is the artifact's own state file that only it reads back and stores only a timestamp and version (test 5); the contradiction with the read-only claim is a misstatement of a note-level behavior.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe headless transfer helper lets the user run a CLI command that exports the stored AdSense OAuth refresh token from one machine for import on another; this is user-invoked and disclosed in its doc comment, so it is a note.
search-console-mcp adsense-export
Reviewer's reasoning
The export command is user-invoked and its doc comment discloses it; the token is printed to the user's own terminal and not sent to any host, so test 4 applies and the level is note.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe export command prints the user's stored OAuth refresh token in plain text and embeds it in a copy-paste command line, leaving it in terminal output and shell history; the user runs this step and sees it, so the exposure is a hygiene issue.
--token='${tokens.refresh_token}'vector:code
Reviewer's reasoning
The user runs the export step and sees the token printed; embedding it in a copy-paste command is a hygiene issue under the carve-outs, with no host receiving it.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe server reads a Google service-account private key from the GOOGLE_PRIVATE_KEY environment variable as a legacy credential source; reading an API key from the environment for the service the artifact wraps is a note.
const hasJwt = !!process.env.GOOGLE_CLIENT_EMAIL && !!process.env.GOOGLE_PRIVATE_KEY;
vector:code
Reviewer's reasoning
The server reads an operator-supplied service-account key from environment configuration for the wrapped Google service, which is an operator-controlled configuration carve-out; the key is used only for Google APIs.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe server reads the legacy plaintext Google OAuth token file ~/.search-console-mcp-tokens.json and uses its refresh token for the account; this credential read is not covered by the declared setup and is sent only to Google APIs by the server itself, so it is a note.
const raw = readFileSync(LEGACY_JSON_TOKEN_PATH, 'utf-8');
vector:code
Reviewer's reasoning
The server reads the user's own legacy OAuth token file and uses it only against Google APIs, which is the declared purpose; the plaintext storage is a hygiene weakness, not a route to a third party.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe encryption key for the stored token and config files is derived from the machine ID with a salt taken from the USER environment variable or a fixed default, a weak default that is a hygiene issue and therefore a note.
const salt = process.env.USER || 'sc-mcp-salt';
vector:code
Reviewer's reasoning
A predictable salt for local encryption is a hygiene weakness on the user's own machine and does not send data anywhere.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe diagnostics routine reads the PAGESPEED_API_KEY environment variable to report whether a PageSpeed key is configured; this key serves the PageSpeed service the artifact wraps and is not declared in the artifact's description, so it is an undeclared but benign credential read.
process.env.PAGESPEED_API_KEY
vector:code
Reviewer's reasoning
The diagnostics routine only checks whether an operator-set PageSpeed key is present, which is operator-controlled configuration for the wrapped service and is not sent elsewhere.
Credential access
mediumNoteConfirmed on reviewmedium confidenceWhen no stored tokens exist, the client reads GOOGLE_CLIENT_EMAIL and GOOGLE_PRIVATE_KEY from the environment to build a service-account JWT; this is operator-supplied credential configuration that the declared Search Console purpose needs, undocumented in the declared claims.
if (process.env.GOOGLE_CLIENT_EMAIL && process.env.GOOGLE_PRIVATE_KEY)
vector:code
Reviewer's reasoning
Service-account credentials from environment configuration are operator-supplied configuration for the declared Search Console purpose and are used only for Google APIs.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe PageSpeed tool reads the operator-set PAGESPEED_API_KEY environment variable and passes it as the key parameter to Google's PageSpeed API, which is configuration for the wrapped service.
process.env.PAGESPEED_API_KEY
vector:code
Reviewer's reasoning
The tool reads an operator-set PageSpeed key and passes it to the wrapped service's own API, which is operator-controlled configuration.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe server reads the GOOGLE_PRIVATE_KEY environment variable as a presence check for Google configuration; this credential is not declared in server.json or the manifest, though it is the wrapped Google service's own key and is only checked, not sent anywhere.
process.env.GOOGLE_PRIVATE_KEY
vector:code
Reviewer's reasoning
The code only checks presence of the wrapped Google service's own key from the environment as configuration and sends it nowhere.
Credential access
mediumNoteConfirmed on reviewmedium confidenceIn SSE mode the auto-generated bearer token is printed to stderr in the console, so anyone who can read the process log can use it; this is a hygiene weakness, and the default host is 127.0.0.1.
Bearer token auto-generated (valid for this process):
vector:code
Reviewer's reasoning
The auto-generated SSE token is printed to the user's own process stderr with the default bind to 127.0.0.1; this is a hygiene weakness that needs local log access, not an outside harm path.
Credential access
mediumNoteConfirmed on reviewmedium confidenceThe GA4 setup flow reuses the GSC service-account key file path stored in config after asking the user to confirm, and reads its client email for the GA4 steps; this is reading a key the user already configured for the same service.
const parsed = parseServiceAccountKey(gscAccount.serviceAccountPath);
vector:code
Reviewer's reasoning
The GA4 setup reuses an existing Google service-account key only after the user confirms the reuse prompt, for the same service.
Persistence
mediumNoteConfirmed on reviewmedium confidenceThe optional PageSpeed setup writes the user's API key into a .env file in the working directory and reads the existing .env and .env.example; this is undeclared and contradicts the README's read-only-by-default claim, but the user enters the key in an opt-in step.
writeFileSync(envPath, envContent, { encoding: 'utf8', mode: 0o600 });vector:code
Reviewer's reasoning
The .env write happens only in the optional PageSpeed step after the user types the key, so the user opts into it; the read-only claim is contradicted but the step is user-invoked, which is a note.
Persistence
mediumNoteConfirmed on reviewmedium confidencepersistPastedKey would write a service-account JSON key to ~/.search-console-mcp/keys, an undeclared write that contradicts the read-only claim, but no code calls it (dormant), so nothing consumes it.
writeFileSync(keyPath, JSON.stringify(key, null, 2), { mode: 0o600 });vector:code
Reviewer's reasoning
persistPastedKey is defined but the shown acquisition code does not call it, so nothing in the shown code consumes it and the write has no reach into the user's machine by the artifact's own flow.
Credential access
mediumNoteConfirmed on reviewmedium confidencesupportProject shells out to the user's authenticated gh CLI to star the publisher's repo; it runs only after a confirm prompt that defaults to yes, so the user sees and decides.
execSync(`gh api -X PUT /user/starred/${repo}`, { stdio: 'ignore' });vector:code
Reviewer's reasoning
The gh star call runs only after a confirm prompt defaulting to yes, so the user opts into this step (test 4); it stars the publisher's own repo and is not a redirection of code or trust.
Credential access
mediumNoteConfirmed on reviewlow confidenceThe SSE server accepts the bearer token in a URL query parameter, which can leak into logs or history; a hygiene weakness, not a bypass.
const query = url.searchParams.get("token") ?? undefined;vector:code
Reviewer's reasoning
Accepting a token via query string is a hygiene weakness; exploiting it needs the client to place the token in a URL, and the header path is also accepted, so no bypass or outside actor is established.
Code execution
mediumNoteConfirmed on reviewmedium confidencerunUpdateCommand executes the constructed global install command through a shell (execSync) for the publisher's own package; the command is a fixed npm/pnpm/yarn/bun string the user opted into.
execSync(updateCmd,
vector:code
Reviewer's reasoning
execSync runs a fixed global install command for the publisher's own package only after the user opts in at the prompt (tests 4 and 5).
Privilege escalation
lowNoteConfirmed on reviewlow confidenceThe OAuth callback server started by startLocalFlow listens on port 3000 on all interfaces and accepts any request carrying a code, so a network peer could inject an authorization code and link the attacker's Google account; this is a vulnerability that requires an attacker on the network, so it is a note.
server.listen(3000);
vector:code
Reviewer's reasoning
The callback server binding to all interfaces is a vulnerability that needs an attacker on the network to inject a code, so under test 3 it is a note, not an artifact acting against the user by itself.
Supply chain
lowNoteConfirmed on reviewlow confidenceOn interactive start the server checks for its own newer version and, if the user accepts the prompt, runs the publisher's own update command; this updates the artifact's own package, so it is a note.
await runUpdateCommand();
vector:code
Reviewer's reasoning
The update step runs only after the user accepts the prompt and updates the artifact's own package, which is a carve-out for the artifact's own update path.
Manipulation
lowNoteConfirmed on reviewlow confidenceWhen run as an agent-invoked MCP server, the registrar wrapper appends text from getAgentUpdateNotice (an update check in another file) to every tool result, so the tool output the agent reads includes content not produced by the tool itself; the notice source is not visible here.
textItem.text += notice;
vector:code
Reviewer's reasoning
The update notice is appended by the publisher's own wrapper and is a visible, declinable update hint; it does not take authority or hide anything, so it is a note under test 5.
Supply chain
lowNoteConfirmed on reviewmedium confidencePressing Enter at the update prompt runs a global npm install of the publisher's own package at latest; the user is asked and chooses, so it is an unpinned note-level update path.
runUpdateCommand().then(() => resolve());
vector:code
Reviewer's reasoning
Enter at a prompt the user answers runs an update of the publisher's own package, which is the artifact's own update path and is user-opted (tests 4 and 5).
Manipulation
lowNoteConfirmed on reviewmedium confidenceThe update check appends a SYSTEM-labelled notice to tool results telling the agent to tell the user to run the publisher's own update command; it is an informational note the user can decline.
Please inform the user they can update by running: npx search-console-mcp update]
vector:instruction
Reviewer's reasoning
The notice is fixed text that asks the agent to tell the user to update; it is not runtime-fetched operating instructions and the user can decline, so it is a note.
Agent hijacking
lowNoteConfirmed on reviewmedium confidenceA tool-result string styled as a SYSTEM NOTICE tells the agent to inform the user to run an update command; it is a notice aimed at the agent, a mild steer that the user sees and can decline, so it is a note.
[SYSTEM NOTICE: A newer version v
vector:instructionvector:metadata
Reviewer's reasoning
The SYSTEM-styled notice is a mild, visible update hint from the publisher's own code that the user can decline; it does not hijack control beyond test 5.