@georanker/seo-mcp 0.14.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.
@@ -0,0 +1,390 @@
1
+ // Only signed release artifacts from the fixed public main workflow may be installed.
2
+ import { execFile } from 'node:child_process';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { access, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
5
+ import { homedir, devNull } from 'node:os';
6
+ import { delimiter, dirname, join, resolve } from 'node:path';
7
+ import { promisify } from 'node:util';
8
+ const exec = promisify(execFile);
9
+ const SHA = /^[a-f0-9]{40}$/;
10
+ const DIGEST = /^[a-f0-9]{64}$/;
11
+ const WORKFLOW = '.github/workflows/client-release.yml';
12
+ const ARTIFACT = 'client-update.tgz';
13
+ export const UPDATE_CHECK_INTERVAL_MS = 5 * 60 * 1000;
14
+ const REPOSITORIES = new Map([
15
+ ['georanker/georanker-seo-mcp', '@georanker/seo-mcp'],
16
+ ['georanker/georanker-web-scraping-mcp', '@georanker/web-scraping-mcp'],
17
+ ]);
18
+ export function updateDirectory(options) {
19
+ if (!REPOSITORIES.has(options.repository))
20
+ throw new Error('Unsupported update repository');
21
+ return join(options.env.GEORANKER_MCP_UPDATE_DIR || join(homedir(), '.config', 'georanker-mcp-updates'), options.repository.split('/')[1]);
22
+ }
23
+ function enabled(options) { return options.env.GEORANKER_MCP_AUTO_UPDATE !== '0'; }
24
+ function older(candidate, bundled) {
25
+ if (!/^\d+\.\d+\.\d+$/.test(candidate) || !/^\d+\.\d+\.\d+$/.test(bundled))
26
+ throw new Error('Unsupported client version');
27
+ const a = candidate.split('.').map(Number), b = bundled.split('.').map(Number);
28
+ for (let i = 0; i < 3; i++) {
29
+ if (a[i] !== b[i])
30
+ return a[i] < b[i];
31
+ }
32
+ return false;
33
+ }
34
+ async function readPointer(directory) {
35
+ try {
36
+ const value = JSON.parse(await readFile(join(directory, 'current.json'), 'utf8'));
37
+ return SHA.test(value.current) && (!value.previous || SHA.test(value.previous)) ? value : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ async function ready(options, commit) {
44
+ if (!SHA.test(commit))
45
+ return undefined;
46
+ try {
47
+ const root = join(updateDirectory(options), 'releases', commit);
48
+ const value = JSON.parse(await readFile(join(root, '.ready.json'), 'utf8'));
49
+ if (value.repository !== options.repository || value.commit !== commit || older(value.version, options.version))
50
+ return undefined;
51
+ await access(join(root, 'dist/src/runtime.js'));
52
+ await access(join(root, 'dist/src/cli.js'));
53
+ return value;
54
+ }
55
+ catch {
56
+ return undefined;
57
+ }
58
+ }
59
+ export async function selectRelease(options) {
60
+ if (enabled(options)) {
61
+ const pointer = await readPointer(updateDirectory(options));
62
+ for (const commit of [pointer?.current, pointer?.previous]) {
63
+ if (commit && await ready(options, commit))
64
+ return { root: join(updateDirectory(options), 'releases', commit), commit };
65
+ }
66
+ }
67
+ return { root: options.bundledRoot };
68
+ }
69
+ async function writePointer(directory, pointer) {
70
+ const temp = join(directory, 'pointer-' + randomUUID() + '.tmp');
71
+ await writeFile(temp, JSON.stringify(pointer) + '\n', { mode: 0o600 });
72
+ await rename(temp, join(directory, 'current.json'));
73
+ }
74
+ export async function rollbackRelease(options, commit) {
75
+ try {
76
+ const directory = updateDirectory(options), pointer = await readPointer(directory);
77
+ if (pointer?.current !== commit)
78
+ return;
79
+ await writeFile(join(directory, 'rejected.json'), JSON.stringify({ commit, retryAfter: Date.now() + 15 * 60 * 1000 }), { mode: 0o600 });
80
+ if (pointer.previous && await ready(options, pointer.previous))
81
+ await writePointer(directory, { current: pointer.previous });
82
+ else
83
+ await rm(join(directory, 'current.json'), { force: true });
84
+ }
85
+ catch {
86
+ options.log?.('Could not record rollback; the bundled client remains available.');
87
+ }
88
+ }
89
+ // This policy is evaluated only after Sigstore verifies the certificate, signature,
90
+ // transparency inclusion and exact workflow identity. No statement field is trusted first.
91
+ export function releaseFromStatement(repository, statement) {
92
+ if (!REPOSITORIES.has(repository))
93
+ throw new Error('Unsupported update repository');
94
+ const s = statement;
95
+ const definition = s?.predicate?.buildDefinition;
96
+ const workflow = definition?.externalParameters?.workflow;
97
+ const source = 'https://github.com/' + repository;
98
+ const subject = s?.subject;
99
+ if (s?._type !== 'https://in-toto.io/Statement/v1' || s?.predicateType !== 'https://slsa.dev/provenance/v1' ||
100
+ definition?.buildType !== 'https://actions.github.io/buildtypes/workflow/v1' ||
101
+ workflow?.repository !== source || workflow?.ref !== 'refs/heads/main' || workflow?.path !== WORKFLOW ||
102
+ !Array.isArray(subject) || subject.length !== 1 || subject[0]?.name !== ARTIFACT || !DIGEST.test(subject[0]?.digest?.sha256)) {
103
+ throw new Error('Release provenance does not match the approved product workflow');
104
+ }
105
+ const dependency = definition?.resolvedDependencies?.find((item) => item.uri === 'git+' + source + '@refs/heads/main');
106
+ const commit = dependency?.digest?.gitCommit;
107
+ if (!SHA.test(commit))
108
+ throw new Error('Release provenance lacks an immutable source commit');
109
+ return { commit, sha256: subject[0].digest.sha256 };
110
+ }
111
+ async function download(url, limit, signal) {
112
+ const timeout = AbortSignal.timeout(30000);
113
+ const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout, credentials: 'omit' });
114
+ if (!response.ok || !response.body || !response.url.startsWith('https://'))
115
+ throw new Error('Update download unavailable');
116
+ const chunks = [];
117
+ let size = 0;
118
+ for await (const chunk of response.body) {
119
+ size += chunk.length;
120
+ if (size > limit)
121
+ throw new Error('Update exceeds size limit');
122
+ chunks.push(Buffer.from(chunk));
123
+ }
124
+ return Buffer.concat(chunks);
125
+ }
126
+ // The cache stores only the most recent fully verified bundle per product. Reusing
127
+ // identical signed bytes avoids repeated Sigstore trust-root requests during polling.
128
+ // Verification failures never enter the cache; the cache does not survive a restart.
129
+ export function createReleaseVerifier(verify = verifyReleaseBundle) {
130
+ const verified = new Map();
131
+ return async (repository, bytes) => {
132
+ if (!REPOSITORIES.has(repository))
133
+ throw new Error('Unsupported update repository');
134
+ const digest = createHash('sha256').update(bytes).digest('hex');
135
+ const cached = verified.get(repository);
136
+ if (cached?.digest === digest)
137
+ return { ...cached.release };
138
+ const release = await verify(repository, JSON.parse(bytes.toString('utf8')));
139
+ verified.set(repository, { digest, release: { ...release } });
140
+ return { ...release };
141
+ };
142
+ }
143
+ const verifyLatest = createReleaseVerifier();
144
+ async function signedLatest(options) {
145
+ const bytes = await download('https://github.com/' + options.repository + '/releases/latest/download/client-update.sigstore.json', 1024 * 1024, options.signal);
146
+ return verifyLatest(options.repository, bytes);
147
+ }
148
+ export async function verifyReleaseBundle(repository, bundle) {
149
+ if (!REPOSITORIES.has(repository))
150
+ throw new Error('Unsupported update repository');
151
+ if (!bundle?.verificationMaterial?.certificate && !bundle?.verificationMaterial?.x509CertificateChain)
152
+ throw new Error('Expected a signed workflow certificate');
153
+ if (!bundle.dsseEnvelope || bundle.dsseEnvelope.payloadType !== 'application/vnd.in-toto+json')
154
+ throw new Error('Expected signed provenance');
155
+ const { verify } = await import('sigstore');
156
+ const identity = 'https://github.com/' + repository + '/' + WORKFLOW + '@refs/heads/main';
157
+ await verify(bundle, {
158
+ certificateIssuer: 'https://token.actions.githubusercontent.com',
159
+ certificateIdentityURI: '^' + identity.replace(/[.*+?^$()|[\]\\]/g, '\\$&') + '$',
160
+ tlogThreshold: 1, ctLogThreshold: 1,
161
+ });
162
+ return releaseFromStatement(repository, JSON.parse(Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString('utf8')));
163
+ }
164
+ export function verifyArtifactDigest(bytes, sha256) {
165
+ if (!DIGEST.test(sha256) || createHash('sha256').update(bytes).digest('hex') !== sha256)
166
+ throw new Error('Release digest mismatch');
167
+ }
168
+ function installEnvironment(options) {
169
+ // No provider keys or installation credentials reach dependency installation.
170
+ const env = {};
171
+ for (const name of ['PATH', 'HOME', 'USERPROFILE', 'SystemRoot', 'SYSTEMROOT', 'WINDIR', 'TEMP', 'TMP', 'TMPDIR', 'LOCALAPPDATA']) {
172
+ if (options.env[name])
173
+ env[name] = options.env[name];
174
+ }
175
+ env.PATH = dirname(process.execPath) + delimiter + (env.PATH || '');
176
+ env.npm_config_userconfig = join(updateDirectory(options), 'empty-user.npmrc');
177
+ env.npm_config_globalconfig = devNull;
178
+ return env;
179
+ }
180
+ async function npmCli() {
181
+ const nodeDirectory = dirname(process.execPath);
182
+ for (const candidate of [resolve(nodeDirectory, 'node_modules/npm/bin/npm-cli.js'), resolve(nodeDirectory, '../lib/node_modules/npm/bin/npm-cli.js')]) {
183
+ try {
184
+ await access(candidate);
185
+ return candidate;
186
+ }
187
+ catch { }
188
+ }
189
+ throw new Error('npm is unavailable beside Node');
190
+ }
191
+ async function prepareSigned(options, release, target) {
192
+ const bytes = await download('https://github.com/' + options.repository + '/releases/download/client-' + release.commit + '/' + ARTIFACT, 20 * 1024 * 1024, options.signal);
193
+ verifyArtifactDigest(bytes, release.sha256);
194
+ // Signature + digest checks precede archive parsing and any code/dependency execution.
195
+ const file = join(target, ARTIFACT);
196
+ await writeFile(file, bytes, { mode: 0o600 });
197
+ const tar = await import('tar');
198
+ let safe = true;
199
+ await tar.t({ file, onReadEntry: entry => {
200
+ const path = entry.path;
201
+ if (!['File', 'Directory'].includes(entry.type) || !path.startsWith('package/') || path.includes('\\') || path.split('/').includes('..'))
202
+ safe = false;
203
+ } });
204
+ if (!safe)
205
+ throw new Error('Unsafe release archive');
206
+ await tar.x({ file, cwd: target, strip: 1, strict: true, preservePaths: false });
207
+ await rm(file);
208
+ await access(join(target, 'npm-shrinkwrap.json'));
209
+ }
210
+ async function validateInstalled(options, target) {
211
+ const env = installEnvironment(options);
212
+ await writeFile(env.npm_config_userconfig, '', { mode: 0o600 });
213
+ const npm = await npmCli();
214
+ await exec(process.execPath, [npm, 'ci', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund', '--cache', join(updateDirectory(options), 'npm-cache')], { cwd: target, env, timeout: 120000, maxBuffer: 4 * 1024 * 1024, signal: options.signal, windowsHide: true });
215
+ await validateConnection(options, target);
216
+ }
217
+ async function validateConnection(options, target) {
218
+ // Disable BOTH selection and background checks to validate this exact candidate.
219
+ // Setup checks enrollment/schemas, never a provider data query.
220
+ await exec(process.execPath, [join(target, 'dist/src/cli.js'), '--setup'], { cwd: target, env: { ...options.env, PATH: installEnvironment(options).PATH, GEORANKER_MCP_AUTO_UPDATE: '0' }, timeout: 30000, maxBuffer: 1024 * 1024, signal: options.signal, windowsHide: true });
221
+ }
222
+ async function acquire(directory) {
223
+ const lock = join(directory, 'update.lock'), token = randomUUID();
224
+ try {
225
+ await mkdir(lock);
226
+ }
227
+ catch {
228
+ try {
229
+ if (Date.now() - (await stat(lock)).mtimeMs < 600000)
230
+ return undefined;
231
+ let owner;
232
+ try {
233
+ owner = JSON.parse(await readFile(join(lock, 'owner.json'), 'utf8'));
234
+ }
235
+ catch { }
236
+ if (owner && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
237
+ try {
238
+ process.kill(owner.pid, 0);
239
+ return undefined;
240
+ }
241
+ catch (error) {
242
+ if (error.code !== 'ESRCH')
243
+ return undefined;
244
+ }
245
+ }
246
+ await rm(lock, { recursive: true, force: true });
247
+ await mkdir(lock);
248
+ }
249
+ catch {
250
+ return undefined;
251
+ }
252
+ }
253
+ await writeFile(join(lock, 'owner.json'), JSON.stringify({ token, pid: process.pid, createdAt: Date.now() }), { mode: 0o600 });
254
+ return {
255
+ async assertHeld() {
256
+ const owner = JSON.parse(await readFile(join(lock, 'owner.json'), 'utf8'));
257
+ if (owner.token !== token)
258
+ throw new Error('Update lock ownership changed');
259
+ },
260
+ async release() {
261
+ const owner = JSON.parse(await readFile(join(lock, 'owner.json'), 'utf8'));
262
+ if (owner.token === token)
263
+ await rm(lock, { recursive: true, force: true });
264
+ },
265
+ };
266
+ }
267
+ export async function checkForUpdate(options, fixture) {
268
+ if (!enabled(options))
269
+ return { status: 'disabled' };
270
+ let lock, staging;
271
+ try {
272
+ const directory = updateDirectory(options);
273
+ await mkdir(directory, { recursive: true, mode: 0o700 });
274
+ await mkdir(join(directory, 'releases'), { recursive: true, mode: 0o700 });
275
+ lock = await acquire(directory);
276
+ if (!lock)
277
+ return { status: 'busy' };
278
+ const now = Date.now();
279
+ if (!options.force) {
280
+ try {
281
+ const previous = JSON.parse(await readFile(join(directory, 'last-check.json'), 'utf8'));
282
+ if (Number.isSafeInteger(previous.checkedAt) && previous.checkedAt <= now &&
283
+ now - previous.checkedAt < UPDATE_CHECK_INTERVAL_MS)
284
+ return { status: 'deferred' };
285
+ }
286
+ catch { }
287
+ }
288
+ // Record attempts before network work, including failures, so reconnects and
289
+ // simultaneous hosts cannot multiply release-feed requests.
290
+ await lock.assertHeld();
291
+ await writeFile(join(directory, 'last-check.json'), JSON.stringify({ checkedAt: now }) + '\n', { mode: 0o600 });
292
+ const backend = fixture || {
293
+ latest: () => signedLatest(options),
294
+ prepare: (release, target) => prepareSigned(options, release, target),
295
+ validate: target => validateInstalled(options, target),
296
+ };
297
+ const release = await backend.latest(), commit = release.commit;
298
+ if (!SHA.test(commit) || !DIGEST.test(release.sha256))
299
+ throw new Error('Invalid signed release');
300
+ const pointer = await readPointer(directory);
301
+ if (pointer?.current === commit && await ready(options, commit))
302
+ return { status: 'current', commit };
303
+ let retryRejected = false;
304
+ try {
305
+ const rejected = JSON.parse(await readFile(join(directory, 'rejected.json'), 'utf8'));
306
+ if (rejected.commit === commit) {
307
+ if (rejected.retryAfter > Date.now())
308
+ return { status: 'failed', commit };
309
+ retryRejected = true;
310
+ }
311
+ }
312
+ catch { }
313
+ const existing = await ready(options, commit);
314
+ let version = existing?.version;
315
+ const currentVersion = pointer?.current ? (await ready(options, pointer.current))?.version : undefined;
316
+ const versionFloor = currentVersion && !older(currentVersion, options.version) ? currentVersion : options.version;
317
+ if (version && older(version, versionFloor))
318
+ throw new Error('Cached update is older than the active client');
319
+ // A temporary hosted outage must not blacklist a valid signed release forever.
320
+ // Revalidate after cooldown without changing its immutable dependency tree.
321
+ if (existing && retryRejected) {
322
+ const candidate = join(directory, 'releases', commit);
323
+ if (fixture)
324
+ await fixture.validate(candidate);
325
+ else
326
+ await validateConnection(options, candidate);
327
+ }
328
+ if (!existing) {
329
+ staging = await mkdtemp(join(directory, 'staging-'));
330
+ await backend.prepare(release, staging);
331
+ const manifest = JSON.parse(await readFile(join(staging, 'package.json'), 'utf8'));
332
+ if (manifest.name !== REPOSITORIES.get(options.repository))
333
+ throw new Error('Update product mismatch');
334
+ version = manifest.version;
335
+ if (typeof version !== 'string' || older(version, versionFloor))
336
+ throw new Error('Update is older than the active client');
337
+ await backend.validate(staging);
338
+ await access(join(staging, 'dist/src/runtime.js'));
339
+ await access(join(staging, 'dist/src/cli.js'));
340
+ await writeFile(join(staging, '.ready.json'), JSON.stringify({ repository: options.repository, commit, version, sha256: release.sha256 }), { mode: 0o600 });
341
+ await lock.assertHeld();
342
+ await rename(staging, join(directory, 'releases', commit));
343
+ staging = undefined;
344
+ }
345
+ await lock.assertHeld();
346
+ const active = await readPointer(directory);
347
+ const activeVersion = active?.current ? (await ready(options, active.current))?.version : undefined;
348
+ if (activeVersion && version && older(version, activeVersion))
349
+ throw new Error('A newer client was activated during this update');
350
+ await lock.assertHeld();
351
+ await writePointer(directory, { current: commit, ...(active?.current && active.current !== commit ? { previous: active.current } : {}) });
352
+ options.log?.('Verified client update ' + version + ' is ready to apply when idle.');
353
+ return { status: 'prepared', commit, version };
354
+ }
355
+ catch {
356
+ return { status: 'failed' };
357
+ }
358
+ finally {
359
+ if (staging)
360
+ await rm(staging, { recursive: true, force: true }).catch(() => { });
361
+ if (lock)
362
+ await lock.release().catch(() => { });
363
+ }
364
+ }
365
+ export function startUpdateChecks(options, onChecked) {
366
+ if (!enabled(options))
367
+ return () => { };
368
+ const controller = new AbortController();
369
+ let running = false;
370
+ const check = async () => {
371
+ if (running || controller.signal.aborted)
372
+ return;
373
+ running = true;
374
+ try {
375
+ const result = await checkForUpdate({ ...options, signal: controller.signal, force: false, log: undefined });
376
+ if (!controller.signal.aborted)
377
+ await onChecked?.(result);
378
+ }
379
+ catch {
380
+ // Optional background housekeeping must not interrupt the MCP or log checks.
381
+ }
382
+ finally {
383
+ running = false;
384
+ }
385
+ };
386
+ void check();
387
+ const timer = setInterval(() => { void check(); }, UPDATE_CHECK_INTERVAL_MS);
388
+ timer.unref();
389
+ return () => { clearInterval(timer); controller.abort(); };
390
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ // Internal worker entry. A supervisor owns the host's stdio connection.
2
+ import { run } from './runtime.js';
3
+ run().catch((error) => {
4
+ const value = error;
5
+ if (['REMOTE_SCHEMA_MISMATCH', 'REMOTE_PROFILE_MISMATCH', 'REMOTE_VERSION_MISMATCH', 'CLIENT_VERSION_MISMATCH'].includes(String(value?.code)) && typeof value?.message === 'string') {
6
+ // A bounded protocol diagnostic for the parent, never routine user-facing logs.
7
+ process.stderr.write('GEORANKER_WORKER_ERROR ' + JSON.stringify({ code: value.code, message: value.message.slice(0, 8192) }) + '\n');
8
+ }
9
+ process.exitCode = 1;
10
+ });
@@ -0,0 +1,23 @@
1
+ **SEO & SERP MCP by GeoRanker: first workflow**
2
+
3
+ Show up to 20 Google organic results for car insurance in Bucharest, Romania, with positions, source URLs and freshness.
4
+
5
+ Call search_serps with this input to request live fetching:
6
+
7
+ ```json
8
+ {
9
+ "query": "car insurance",
10
+ "region": "Bucharest,Romania",
11
+ "searchEngine": "google",
12
+ "pages": 2,
13
+ "forceLive": true
14
+ }
15
+ ```
16
+
17
+ Omit forceLive or set it to false to allow seven-day completed-cache reuse. Live fetching bypasses the MCP cache, not the provider's own processing rules. If pending, call get_serp_result with the same jobId. Lookups never start another data job.
18
+
19
+ Requested depth is at most 100 organic results; returned coverage may be lower. An absent domain is not found within the returned results, not a measured position beyond them. Use create_rank_tracking_report for multi-keyword/multi-location domain tracking and optional explicit recurring schedules. Lighthouse audits, internal-link crawls, backlink reports and keyword-volume reports have separate create/get tools. Recurring runs require operator enablement and can consume future credits. For an existing recurring rank report, forceLive or expired ready cache retrieves the same report and returns refreshNotice; it does not request a manual capture or create another schedule. Lighthouse output is a labeled summary without screenshots or detailed resource/audit tables. Report completion and provider coverage are returned as supplied; unknown completion is not a successful result claim.
20
+
21
+ Display cached, cachedAt and generatedAt when available. A missing provider generation timestamp must not be invented. The default does not mean background refresh every seven days.
22
+
23
+ Request independent tasks in parallel. The hosted service applies shared and per-installation limits and may briefly queue a call. Clients sharing an installation share its limits; the operator controls slots centrally. Keep pending job or report IDs and retrieve existing results. Cancelling a call does not guarantee that submitted work stopped. Completed-result reuse remains seven days by default.
@@ -0,0 +1,160 @@
1
+ **Install SEO & SERP MCP by GeoRanker**
2
+
3
+ The client is MIT-licensed. npm is the primary installation route for released versions; GitHub source installation is the fallback. If the requested npm version is not yet available, use the source instructions below.
4
+
5
+ **Primary installation: npm**
6
+
7
+ Use Node.js and npm, including npx. Supported Node.js versions are 22.22.2+ on the 22 release line, 24.15.0+ on the 24 release line, or 26.0.0+. Git and a TypeScript build are not required for the npm route.
8
+
9
+ Run the setup check before configuring your host:
10
+
11
+ ```sh
12
+ npx --yes @georanker/seo-mcp@0.14.0 --setup
13
+ ```
14
+
15
+ These commands require @georanker/seo-mcp@0.14.0 to be available on the public npm registry. If that release is not yet published, use the GitHub source installation below. npx may need registry access to download the package and its dependencies.
16
+
17
+ The setup check verifies enrollment and the expected tools without a data query. Set GEORANKER_MCP_URL only when using a different endpoint. An unavailable or mismatched endpoint must be fixed by the operator; do not delete installation state to retry.
18
+
19
+ The host examples below use the same npm bootstrap version. If a graphical application cannot find npx, use the absolute path to its executable and follow the host's platform-specific launch instructions. Preserve unrelated configuration entries. Reconnect the host after changes, enable/trust the tools when requested, then run the sample prompt.
20
+
21
+ **GitHub source installation (fallback)**
22
+
23
+ Use Git and the supported Node.js/npm runtime to build once:
24
+
25
+ ```sh
26
+ git clone https://github.com/georanker/georanker-seo-mcp.git georanker-seo-mcp
27
+ cd georanker-seo-mcp
28
+ npm ci
29
+ npm run build
30
+ node dist/src/cli.js --setup
31
+ ```
32
+
33
+ For a source installation, replace each npx command and its package arguments in the host examples with node and the absolute path to this checkout's dist/src/cli.js. For example, use "command": "node" and "args": ["/ABSOLUTE/PATH/dist/src/cli.js"] in an mcpServers entry. If the application cannot find Node, set command to the absolute node executable path. Keep that launcher path stable for future updates.
34
+
35
+ **Automatic updates**
36
+
37
+ Keep your host configured to the same npm bootstrap command or source launcher. Starting with client 0.13.0, it checks for released updates from the public georanker/georanker-seo-mcp repository at startup and every five minutes while the MCP is running. A push to main automatically runs the repository's release workflow. Only after its tests and clean-install checks pass does that workflow publish the client release with signed GitHub provenance.
38
+
39
+ The @0.14.0 in the npx command pins the npm bootstrap package. It does not disable GeoRanker's automatic updater: the launcher can select a newer verified release from its separate update cache. To keep running exactly the selected npm version, also set GEORANKER_MCP_AUTO_UPDATE=0 in the host's MCP environment, then restart or reconnect. Manage future version changes explicitly in that configuration.
40
+
41
+ From 0.13.1, a version or schema compatibility error triggers an immediate signed release check without waiting for the five-minute interval. Runtime recovery checks are limited to once per worker release per session; ordinary errors do not trigger them. Active calls remain protected and failed requests are never replayed.
42
+
43
+ The launcher verifies the package's signed provenance against the expected public repository, main-branch release workflow and commit, then checks its artifact checksum. It prepares the verified package in a separate cache and installs its locked production dependencies without lifecycle scripts. It does not download and execute an unverified branch checkout. A stable supervisor keeps the host MCP connection open and runs tools through an internal worker. A verified candidate replaces the worker when no tool calls are active and 60 seconds have passed without tool activity. This idle period concerns the MCP connection, not the whole AI host or existing provider jobs. Calls are never replayed as part of a swap. If the candidate fails, the current worker continues. The prepared version is also available on later launches when GitHub is unavailable.
44
+
45
+ Updates require a supported Node.js version and npm on the MCP process's PATH, access to GitHub, the npm registry and the signature verification service, and write access to the update cache. Git and a TypeScript build are only needed for the source fallback, not for npm installation or automatic updates. The default cache is ~/.config/georanker-mcp-updates/georanker-seo-mcp. Set GEORANKER_MCP_UPDATE_DIR to choose another cache root. This is separate from your existing identity and credentials, which are preserved.
46
+
47
+ With automatic updates enabled, prepare the latest signed release immediately:
48
+
49
+ ```sh
50
+ npx --yes @georanker/seo-mcp@0.14.0 --update
51
+ ```
52
+
53
+ For the source fallback, use:
54
+
55
+ ```sh
56
+ node dist/src/cli.js --update
57
+ ```
58
+
59
+ A running 0.13.0+ supervisor applies a prepared compatible worker during the next idle period. After a cancelled or timed-out worker request, automatic worker swaps wait for a normal host reconnect because completion is uncertain. Changes to supervisor code itself take effect on a normal host restart or MCP reconnect. Set GEORANKER_MCP_AUTO_UPDATE=0 in the host's MCP environment to disable automatic updates. A failed release workflow, download, signature verification or setup check leaves the available client version in place. Automatic checks are quiet when no update is available or a check cannot complete. An applied update is reported on stderr, separate from the MCP protocol.
60
+
61
+ **Migration for existing installations**
62
+
63
+ Existing 0.12.0 installations can prepare a current signed release automatically and load its 0.13.0+ supervisor on the next host restart or MCP reconnect. After that one reconnect, compatible worker updates apply inside the session during idle periods. Future changes to the supervisor itself still require a normal restart.
64
+
65
+ To switch an existing source installation to npm, run the npm setup command above once the package version is published, replace the host's node/source-path command with the matching npx command below, and reconnect. Keep the same GEORANKER_STATE_DIR, service origin and environment settings. Both routes reuse the existing installation identity and credentials; do not delete state or create a second installation to migrate.
66
+
67
+ Clients older than 0.12.0 cannot update themselves. Switch to npm as above, or update the existing source checkout of the public repository and run this once:
68
+
69
+ ```sh
70
+ git pull --ff-only
71
+ npm ci
72
+ npm run build
73
+ node dist/src/cli.js --setup
74
+ ```
75
+
76
+ Then restart or reconnect the MCP in your host. Keep the existing launcher path and credentials. Subsequent compatible worker updates that pass the public main release workflow are prepared and applied during idle periods, without another reinstall.
77
+
78
+ **Codex**
79
+
80
+ ```sh
81
+ codex mcp add georanker-seo -- npx --yes @georanker/seo-mcp@0.14.0
82
+ ```
83
+
84
+ Use /mcp to inspect the connection. [Official guide](https://developers.openai.com/codex/mcp).
85
+
86
+ **Claude Code**
87
+
88
+ ```sh
89
+ claude mcp add --scope user --transport stdio georanker-seo -- npx --yes @georanker/seo-mcp@0.14.0
90
+ ```
91
+
92
+ Use /mcp to inspect the connection. [Official guide](https://code.claude.com/docs/en/mcp).
93
+
94
+ **Hosts using an mcpServers object**
95
+
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "georanker-seo": {
100
+ "command": "npx",
101
+ "args": ["--yes", "@georanker/seo-mcp@0.14.0"]
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ | Host | Where to add the entry | Extra step |
108
+ | --- | --- | --- |
109
+ | [Claude Desktop](https://claude.com/docs/connectors/building/mcpb) | Developer MCP configuration opened through application settings | Restart/reconnect; a tested .mcpb installer is planned, not supplied in this release |
110
+ | [Cursor](https://prod.cursor.com/docs/mcp) | ~/.cursor/mcp.json or project .cursor/mcp.json | Add type: stdio to the server entry; enable it |
111
+ | [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp) | Cascade MCP configuration, normally ~/.codeium/windsurf/mcp_config.json | Verify the current app/version path, then refresh tools |
112
+ | [Cline](https://docs.cline.bot/mcp/mcp-overview) | MCP Servers > Configure > Configure MCP Servers | Save and reconnect |
113
+ | [Continue](https://docs.continue.dev/customize/deep-dives/mcp) | .continue/mcpServers/mcp.json | Use Agent mode |
114
+ | [Gemini CLI](https://geminicli.com/docs/tools/mcp-server/) | Merge into ~/.gemini/settings.json | Inspect /mcp; user scope shown |
115
+ | [OMP / Oh My Pi](https://github.com/can1357/oh-my-pi/blob/main/docs/mcp-config.md) | ~/.omp/agent/mcp.json or project .omp/mcp.json | Add type: stdio; inspect /mcp and avoid duplicate imported entries |
116
+
117
+ **VS Code / GitHub Copilot**
118
+
119
+ Use “MCP: Add Server,” or .vscode/mcp.json with a servers object:
120
+
121
+ ```json
122
+ {
123
+ "servers": {
124
+ "georanker-seo": {
125
+ "type": "stdio",
126
+ "command": "npx",
127
+ "args": ["--yes", "@georanker/seo-mcp@0.14.0"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ Start/trust the server and enable its tools. [Official guide](https://code.visualstudio.com/docs/agent-customization/mcp-servers).
134
+
135
+ **OpenCode**
136
+
137
+ Merge into opencode.json:
138
+
139
+ ```json
140
+ {
141
+ "$schema": "https://opencode.ai/config.json",
142
+ "mcp": {
143
+ "georanker-seo": {
144
+ "type": "local",
145
+ "command": ["npx", "--yes", "@georanker/seo-mcp@0.14.0"],
146
+ "enabled": true
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ Run --setup before adding the entry to avoid first-enrollment work during the host's short discovery timeout. [Official guide](https://opencode.ai/docs/mcp-servers/).
153
+
154
+ **First prompt**
155
+
156
+ > Show up to 20 Google organic results for car insurance in Bucharest, Romania, with positions, source URLs and freshness.
157
+
158
+ By default, eligible completed data can be reused for up to seven days. Add “Force a live fetch” to request forceLive: true. That bypasses the MCP's completed cache; pending work can be reused and should be retrieved using get_serp_result. It does not schedule automatic refreshes or guarantee instant completion.
159
+
160
+ Host configuration formats above are documented compatibility routes. Local protocol fixtures do not prove each host or operating system has been exercised. Cloud-only connections require a separately tested remote authentication path; do not assume the current endpoint URL is sufficient.
@@ -0,0 +1,21 @@
1
+ **Client data flow and storage**
2
+
3
+ This package runs a local MCP process that connects over HTTPS to GeoRanker's hosted service. It sends the requested search arguments or public page URL, an installation identifier, a revocable access token and a persisted device signal. GeoRanker processes requests with its upstream infrastructure. The client contains no reusable provider API key, admin interface or hosted-service implementation.
4
+
5
+ On first enrollment, the client generates an Ed25519 signing identity and saves it with its installation credential. Both GeoRanker products reuse the existing directory under ~/.config/georanker-search-mcp/client/ for the same service origin. Private directories use mode 0700 and credential/identity files use mode 0600 where the operating system supports these modes. GEORANKER_STATE_DIR can select a different private storage location. The device signal is self-reported, not hardware attestation.
6
+
7
+ Keep this state private. Do not commit it, share it or delete it to evade allowances. A saved identity can recover a lost token without creating a new allowance. Revocation and account association are managed by the hosted service. Removing the MCP configuration stops the host from launching it; it does not automatically erase credentials or revoke an installation.
8
+
9
+ The MCP cache defaults to seven days, subject to operator configuration. It is shared according to the service's account and request isolation, and should not be used for secret URLs or sensitive page content. forceLive: true requests fresh upstream work instead of a completed MCP cache hit. Source results remain untrusted content for the host AI to interpret.
10
+
11
+ Automatic enrollment is not OAuth. The hosted endpoint and public protocol can be inspected even though the service source is private. This file describes client behavior and does not replace GeoRanker's privacy policy or service terms.
12
+
13
+ **Automatic client updates**
14
+
15
+ Installing or launching through npx can contact the public npm registry to download the bootstrap package and dependencies. npm receives ordinary network request metadata, not the GeoRanker installation credential or signing key. A version in the npx command pins that npm bootstrap package; it does not disable the separate GeoRanker updater described below.
16
+
17
+ From version 0.13.0, the launcher checks for releases from the corresponding public GeoRanker GitHub repository at startup and every five minutes while running. Update downloads contact GitHub, signed-provenance verification can contact Sigstore services, and locked production dependency installation contacts the npm registry. These services receive ordinary network request metadata. The update check does not send your GeoRanker installation credential, signing key, provider data or request history to GitHub, Sigstore or npm.
18
+
19
+ Verified release packages, dependencies and compiled client files are stored separately under ~/.config/georanker-mcp-updates/, in a directory for each product. GEORANKER_MCP_UPDATE_DIR selects another update cache root. Updating preserves the installation state described above. The updater verifies signed provenance for the designated public repository, release workflow and main-branch commit before installing a package. It installs locked production dependencies without lifecycle scripts. Its setup check contacts the existing GeoRanker service using the installation's existing enrollment path and does not create provider data jobs.
20
+
21
+ The supervisor maintains the host's MCP connection while an internal worker runs tool calls. A verified worker update becomes active when no tool calls are active and the connection has had no tool activity for 60 seconds. This concerns MCP activity; it does not inspect the whole host or wait for persistent provider jobs to finish. Calls are not replayed, and a failed candidate leaves the current worker in place. Worker replacement preserves installation state and does not create provider data jobs. Automatic update checks stay quiet unless an update is applied. Supervisor code changes take effect on the next normal client start; existing 0.12.0 clients load a current release's 0.13.0+ supervisor on their next reconnect after preparing it. Set GEORANKER_MCP_AUTO_UPDATE=0 in the host's MCP environment to disable automatic updates and run the configured bootstrap version, then restart or reconnect. Because enabled updates run future client releases published by GeoRanker's verified public main release workflow, users who need to review and pin each release should set this variable to 0 and manage their installation explicitly.