Announcing Beacon: Self-improving memory for coding agents
← All MCP servers
MCP server

Search Console MCP

mcp:saurabhsharma2u/search-console-mcp

Google Search Console + Bing + GA4 + AdSense — unified SEO intelligence for AI agents.

Verdict
Inconclusive

The analysis could not cover enough of the artifact to decide.

Analyzed Oct 29, 2026, 15:55 UTC at 7de2215

Dispute security verdict

If you believe this verdict is incorrect, submit a dispute and we'll review it.

0
findings
—
highest severity
0
threat classes
266
files scanned
858/858
checks satisfied
0
package advisories

Why this verdict

  • inconclusive: gap dynamic_metadata at src/utils/cli.ts
  • inconclusive: gap parse_error at src/adsense/transfer.ts
  • inconclusive: gap parse_error at src/setup/flows/adsense.ts

Security findings 0

No individual findings recorded

The inconclusive verdict rests on the open questions and coverage gaps below.

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 confidence

On 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

The 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 confidence

When 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 confidence

The 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 confidence

The 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 confidence

In 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 confidence

The 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 confidence

The 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 confidence

persistPastedKey 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 confidence

supportProject 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 confidence

The 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 confidence

runUpdateCommand 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 confidence

The 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 confidence

On 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 confidence

When 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 confidence

Pressing 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 confidence

The 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 confidence

A 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.

Third-party flags 1

Behavior aimed at other sites, platforms or people, such as evading rate limits or offensive-security tooling. It does not affect the person who installs it, so it never changes the verdict.

Credential access

mediumNoteConfirmed on reviewhigh confidence

The Search Console client ships a hardcoded Google OAuth client secret as the fallback for DEFAULT_CLIENT_SECRET, used by every token refresh and OAuth2 client; this is an undeclared embedded publisher credential, a hygiene weakness rather than harm to the installer.

'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'
vector:code
Reviewer's reasoning

The embedded OAuth client secret is the publisher's own desktop-client value, used only against Google's own endpoints; it does not take the installer's credentials, so any harm falls on the publisher rather than the user.

Open questions 2

What the analysis could not settle from the code alone.

Which tool names and descriptions do the call sites of the cli.ts server.tool wrapper pass? The computed name and description cannot be reviewed statically, so it is unknown whether any registered tool shadows or alters another tool's behavior.
Tool poisoning
The registered tool names, descriptions, and input schemas are passed in at runtime by callers of createToolRegistrar, so the tool metadata the client receives cannot be fixed statically here. Their actual content would settle whether any tool poisons or shadows another tool's behavior.
Tool poisoning

Execution chain 90

What it calls, runs, installs, reads and writes, with the line that does it.

Environment variables 15

BING_API_KEY
GOOGLE_APPLICATION_CREDENTIALS
GOOGLE_CLIENT_EMAIL
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
GOOGLE_PRIVATE_KEY
MCP_HTTP_ALLOWED_ORIGINS
MCP_HTTP_HOST
MCP_HTTP_TOKEN
NODE_ENV
npm_config_user_agent
PAGESPEED_API_KEY

Commands run 3

<dynamic>
gh api -X PUT /user/starred/{…}
git remote get-url origin

Files read 7

.env.example
<dynamic>
~/.search-console-mcp-config.enc
~/.search-console-mcp-tokens.enc
~/.search-console-mcp-tokens.json
~/.search-console-mcp-update-cache.json

Files written 5

<dynamic>
~/.search-console-mcp-config.enc
~/.search-console-mcp-update-cache.json
~/.search-console-mcp/keys

GitHub repositories 1

saurabhsharma2u/search-console-mcp

Network calls 8

<dynamic>
api.indexnow.org
indexing.googleapis.com
localhost
oauth2.googleapis.com
registry.npmjs.org
ssl.bing.com
src/bing/client.ts:91 · url_literal
www.googleapis.com

Packages 1

npm:search-console-mcp@2.1.4
server.json:16 · launch:server.json

URLs 50

http://127.0.0.1:$
http://localhost:$
http://localhost:3000/oauth2callback
https://adsense.google.com$
https://analytics.google.com
https://api.indexnow.org/indexnow
https://console.cloud.google.com/apis/credentials
https://console.cloud.google.com/iam-admin/serviceaccounts
https://example.com/blog
https://example.com/blog/my-article
https://example.com/blog/my-post
https://example.com/blog/page-1
https://example.com/coffee-guide
https://example.com/jobs/role-1
https://example.com/jobs/role-2
https://example.com/jobs/software-engineer
https://example.com/my-page
https://example.com/page
https://example.com/shop/
https://example.com/Shop/
https://example.com/sitemap.xml
https://example.com/specific-page
https://example.com/troublesome-page
https://github.com/$
https://github.com/saurabhsharma2u/search-console-mcp
https://github.com/saurabhsharma2u/search-console-mcp.git
https://github.com/saurabhsharma2u/search-console-mcp/issues
https://github.com/saurabhsharma2u/search-console-mcp#pagespeed-insights-optional-api-key$
https://indexing.googleapis.com/v3/urlNotifications
https://localhost:$
https://oauth2.googleapis.com/device/code
https://registry.npmjs.org/search-console-mcp/latest
https://search.google.com/search-console
https://searchconsolemcp.saurabh.app
https://ssl.bing.com/webmaster/api.svc/json
https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json
https://www.bing.com/webmasters/settings/api
https://www.example.com/new-page
https://www.googleapis.com/auth/adsense.readonly
https://www.googleapis.com/auth/analytics.readonly
https://www.googleapis.com/auth/indexing
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/webmasters
https://www.googleapis.com/auth/webmasters.readonly
https://www.indexnow.org/
https://www.searchenginejournal.com/google-algorithm-history/

Coverage gaps 5

Parts of the artifact the static analysis could not fully examine.

parse_error tree-sitter typescript parse has errors; extraction may be partial
src/adsense/transfer.ts
parse_error tree-sitter typescript parse has errors; extraction may be partial
src/setup/flows/adsense.ts
dynamic_metadata line 22: tool (computed name) name depends on unsupported construct (unsupported): anonymous function parameter: (name, description, schemaShape, handler) => { const wrappedHandler = async (args: any) => { const res = await handler(…
src/utils/cli.ts
dynamic_metadata line 22: tool (computed name) description depends on unsupported construct (unsupported): anonymous function parameter: (name, description, schemaShape, handler) => { const wrappedHandler = async (args: any) => { const res = await handler(…
src/utils/cli.ts
dynamic_metadata line 22: tool (computed name) input schema depends on unsupported construct (unsupported): anonymous function parameter: (name, description, schemaShape, handler) => { const wrappedHandler = async (args: any) => { const res = await handler(…
src/utils/cli.ts