pi-mcp-adapter 2.16.0 → 2.18.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.
package/init.ts CHANGED
@@ -147,6 +147,7 @@ export async function initializeMcp(
147
147
  const serverInstructions = new Map<string, string>();
148
148
  const failureTracker = new Map<string, number>();
149
149
  const failureMessages = new Map<string, string>();
150
+ const approvedToolCalls = new Map<string, true>();
150
151
  const uiResourceHandler = new UiResourceHandler(manager, config);
151
152
  const consentManager = new ConsentManager("once-per-server");
152
153
  const state: McpExtensionState = {
@@ -164,6 +165,7 @@ export async function initializeMcp(
164
165
  authStorageOptions,
165
166
  failureTracker,
166
167
  failureMessages,
168
+ approvedToolCalls,
167
169
  uiResourceHandler,
168
170
  consentManager,
169
171
  uiServer: null,
@@ -261,7 +263,8 @@ export async function initializeMcp(
261
263
  });
262
264
 
263
265
  if (ui && startupServers.length > 0) {
264
- ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`));
266
+ const status = formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`);
267
+ ui.setStatus("mcp", status);
265
268
  }
266
269
 
267
270
  const results = await parallelLimit(startupServers, 10, async ([name, definition]) => {
@@ -399,6 +402,9 @@ export async function initializeMcp(
399
402
 
400
403
  owner.throwIfInactive();
401
404
  lifecycle.startHealthChecks(runtimeSignal);
405
+ if (config.settings?.mcpFooterStatus === "off") {
406
+ ui?.setStatus("mcp", undefined);
407
+ }
402
408
  publishMcpStatusSnapshot(state);
403
409
 
404
410
  return state;
@@ -524,10 +530,24 @@ export function updateStatusBar(state: McpExtensionState): void {
524
530
  const definition = state.config.mcpServers[name];
525
531
  return connection.status === "connected" && definition !== undefined && !isServerDisabled(definition);
526
532
  }).length;
527
- let status = `${enabledCount} ${enabledCount === 1 ? "server" : "servers"} enabled`;
528
- if (connectedCount > 0) status += ` (${connectedCount} connected)`;
529
- if (disabledCount > 0) status += ` (${disabledCount} disabled)`;
530
- const formattedStatus = formatMcpStatus(state.config, status);
533
+ const footerStatus = state.config.settings?.mcpFooterStatus ?? "full";
534
+ if (footerStatus === "off") {
535
+ ui.setStatus("mcp", undefined);
536
+ return;
537
+ }
538
+
539
+ let status = footerStatus === "compact"
540
+ ? `MCP ${connectedCount}/${enabledCount}`
541
+ : `${enabledCount} ${enabledCount === 1 ? "server" : "servers"} enabled`;
542
+ if (footerStatus === "full") {
543
+ if (connectedCount > 0) status += ` (${connectedCount} connected)`;
544
+ if (disabledCount > 0) status += ` (${disabledCount} disabled)`;
545
+ }
546
+ const formattedStatus = footerStatus === "compact" ? status : formatMcpStatus(state.config, status);
547
+ if (formattedStatus === undefined) {
548
+ ui.setStatus("mcp", undefined);
549
+ return;
550
+ }
531
551
  ui.setStatus("mcp", ui.theme ? ui.theme.fg("accent", formattedStatus) : formattedStatus);
532
552
  }
533
553
 
@@ -565,7 +585,8 @@ export async function lazyConnect(state: McpExtensionState, serverName: string,
565
585
 
566
586
  try {
567
587
  if (state.ui) {
568
- state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
588
+ const status = formatMcpStatus(state.config, `connecting to ${serverName}...`);
589
+ state.ui.setStatus("mcp", status);
569
590
  }
570
591
  const newConnection = await state.manager.connect(serverName, definition, ownedSignal);
571
592
  if (newConnection.status === "needs-auth") {
package/mcp-auth-flow.ts CHANGED
@@ -57,6 +57,10 @@ type AuthDiscovery = {
57
57
  scope?: string
58
58
  }
59
59
 
60
+ function applyConfiguredScope(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
61
+ return config.scope !== undefined ? { ...discovery, scope: config.scope } : discovery
62
+ }
63
+
60
64
  type PendingAuth = {
61
65
  serverName: string
62
66
  authProvider: McpOAuthProvider
@@ -162,6 +166,18 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
162
166
  if (typeof definition.oauth.scope !== "string") throw new Error("OAuth scope must be a string")
163
167
  config.scope = interpolateEnvVars(definition.oauth.scope)
164
168
  }
169
+ if (definition.oauth?.authorizationParams !== undefined) {
170
+ const params = definition.oauth.authorizationParams
171
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
172
+ throw new Error("OAuth authorizationParams must be an object")
173
+ }
174
+ config.authorizationParams = {}
175
+ for (const [key, value] of Object.entries(params)) {
176
+ if (!key) throw new Error("OAuth authorizationParams keys must not be empty")
177
+ if (typeof value !== "string") throw new Error(`OAuth authorizationParams.${key} must be a string`)
178
+ config.authorizationParams[key] = interpolateEnvVars(value)
179
+ }
180
+ }
165
181
  if (definition.oauth?.redirectUri !== undefined) {
166
182
  if (typeof definition.oauth.redirectUri !== "string") {
167
183
  throw new Error("OAuth redirectUri must be a string")
@@ -304,7 +320,7 @@ export async function startAuth(
304
320
  },
305
321
  }, authStorageOptions, runtime.signal)
306
322
  try {
307
- const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
323
+ const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
308
324
  throwIfAborted(signal)
309
325
  const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
310
326
  throwIfAborted(signal)
@@ -370,7 +386,7 @@ export async function startAuth(
370
386
 
371
387
  throwIfAborted(signal)
372
388
 
373
- const discovery = await probeAuthDiscovery(serverUrl, definition, signal)
389
+ const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
374
390
  throwIfAborted(signal)
375
391
  const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
376
392
  throwIfAborted(signal)
package/mcp-auth.ts CHANGED
@@ -10,10 +10,12 @@
10
10
  * then the plaintext file is removed.
11
11
  */
12
12
 
13
+ import { spawnSync } from 'child_process';
13
14
  import { createHash } from 'crypto';
14
15
  import { createRequire } from 'module';
15
16
  import { readFileSync, existsSync, rmSync } from 'fs';
16
- import { join } from 'path';
17
+ import { dirname, join } from 'path';
18
+ import { fileURLToPath } from 'url';
17
19
  import { getAgentPath } from './agent-dir.ts';
18
20
  import { resolveConfiguredOAuthDir } from './config.ts';
19
21
 
@@ -21,6 +23,12 @@ const require = createRequire(import.meta.url);
21
23
  const AUTH_SECRET_SERVICE = 'pi-mcp-adapter.oauth';
22
24
  const TEST_AUTH_STORE_ENV = 'PI_MCP_ADAPTER_TEST_AUTH_STORE';
23
25
  const AUTH_SECRET_CHUNK_SIZE = 1800;
26
+ const KEYRING_RECOVERY_DISABLED_ENV = 'PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY';
27
+ const KEYRING_RECOVERY_KEYCTL_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_KEYCTL';
28
+ const KEYRING_RECOVERY_NODE_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_NODE';
29
+ const KEYRING_RECOVERY_HELPER_ENV = 'PI_MCP_ADAPTER_KEYRING_RECOVERY_HELPER';
30
+ const TEST_LINUX_KEYRING_RECOVERY_ENV = 'PI_MCP_ADAPTER_TEST_LINUX_KEYRING_RECOVERY';
31
+ const KEYRING_RECOVERY_TIMEOUT_MS = 10_000;
24
32
  const AUTH_CHUNK_MANIFEST_KEY = '__piMcpAdapterOAuthChunked';
25
33
 
26
34
  /** OAuth token storage format */
@@ -113,6 +121,8 @@ interface KeyringEntry {
113
121
  }
114
122
 
115
123
  type KeyringEntryConstructor = new (service: string, account: string) => KeyringEntry;
124
+ type KeyringModule = { Entry: KeyringEntryConstructor };
125
+ type KeyringRequire = ((id: string) => unknown) & { resolve(id: string): string };
116
126
 
117
127
  interface AuthSecretStore {
118
128
  read(account: string): string | undefined;
@@ -165,6 +175,22 @@ const unavailableAuthSecretStore: AuthSecretStore = {
165
175
  },
166
176
  };
167
177
 
178
+ function createKeyRevokedTestError(): Error {
179
+ return new Error("Couldn't access platform storage: KeyRevoked", { cause: new Error('KeyRevoked') });
180
+ }
181
+
182
+ const keyRevokedAuthSecretStore: AuthSecretStore = {
183
+ read() {
184
+ throw createKeyRevokedTestError();
185
+ },
186
+ write() {
187
+ throw createKeyRevokedTestError();
188
+ },
189
+ remove() {
190
+ throw createKeyRevokedTestError();
191
+ },
192
+ };
193
+
168
194
  export function resetTestAuthSecretStore(): void {
169
195
  memoryAuthEntries.clear();
170
196
  }
@@ -180,18 +206,156 @@ export function removeTestAuthSecretStoreEntry(account: string): void {
180
206
  function getAuthSecretStore(): AuthSecretStore {
181
207
  if (process.env[TEST_AUTH_STORE_ENV] === 'memory') return memoryAuthSecretStore;
182
208
  if (process.env[TEST_AUTH_STORE_ENV] === 'unavailable') return unavailableAuthSecretStore;
209
+ if (process.env[TEST_AUTH_STORE_ENV] === 'keyrevoked') return keyRevokedAuthSecretStore;
183
210
  return keyringAuthSecretStore;
184
211
  }
185
212
 
186
213
  function getKeyringEntry(account: string): KeyringEntry {
187
214
  try {
188
- KeyringEntryClass ??= (require('@napi-rs/keyring') as { Entry: KeyringEntryConstructor }).Entry;
215
+ KeyringEntryClass ??= loadKeyringEntryClass();
189
216
  return new KeyringEntryClass(AUTH_SECRET_SERVICE, account);
190
217
  } catch (error) {
191
218
  throw new Error('OAuth secure credential storage is unavailable. Configure the OS credential store and retry authentication.', { cause: error });
192
219
  }
193
220
  }
194
221
 
222
+ function loadKeyringEntryClass(keyringRequire: KeyringRequire = require, platform: NodeJS.Platform = process.platform, arch: NodeJS.Architecture = process.arch): KeyringEntryConstructor {
223
+ try {
224
+ return (keyringRequire('@napi-rs/keyring') as KeyringModule).Entry;
225
+ } catch (loaderError) {
226
+ try {
227
+ return loadKeyringNativeBindingFallback(keyringRequire, platform, arch).Entry;
228
+ } catch (fallbackError) {
229
+ throw new Error(`Failed to load @napi-rs/keyring; absolute-path native binding fallback also failed: ${formatErrorMessage(fallbackError)}`, {
230
+ cause: loaderError,
231
+ });
232
+ }
233
+ }
234
+ }
235
+
236
+ function loadKeyringNativeBindingFallback(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringModule {
237
+ const targets = getKeyringNativeBindingTargets(platform, arch);
238
+ if (targets.length === 0) {
239
+ throw new Error(`Unsupported @napi-rs/keyring native binding target: ${platform}-${arch}`);
240
+ }
241
+
242
+ let lastError: unknown;
243
+ for (const target of targets) {
244
+ try {
245
+ const packageJsonPath = keyringRequire.resolve(`${target.packageName}/package.json`);
246
+ return keyringRequire(join(dirname(packageJsonPath), target.bindingFile)) as KeyringModule;
247
+ } catch (error) {
248
+ lastError = error;
249
+ }
250
+ }
251
+
252
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
253
+ }
254
+
255
+ function getKeyringNativeBindingTargets(platform: NodeJS.Platform, arch: NodeJS.Architecture): { packageName: string; bindingFile: string }[] {
256
+ return getKeyringNativeBindingSuffixes(platform, arch).map(suffix => ({
257
+ packageName: `@napi-rs/keyring-${suffix}`,
258
+ bindingFile: `keyring.${suffix}.node`,
259
+ }));
260
+ }
261
+
262
+ function getKeyringNativeBindingSuffixes(platform: NodeJS.Platform, arch: NodeJS.Architecture): string[] {
263
+ if (platform === 'darwin') {
264
+ if (arch === 'arm64') return ['darwin-arm64'];
265
+ if (arch === 'x64') return ['darwin-x64'];
266
+ }
267
+ if (platform === 'win32') {
268
+ if (arch === 'arm64') return ['win32-arm64-msvc'];
269
+ if (arch === 'x64') return ['win32-x64-msvc'];
270
+ if (arch === 'ia32') return ['win32-ia32-msvc'];
271
+ }
272
+ if (platform === 'linux') {
273
+ if (arch === 'arm64') return ['linux-arm64-gnu', 'linux-arm64-musl'];
274
+ if (arch === 'arm') return ['linux-arm-gnueabihf'];
275
+ if (arch === 'riscv64') return ['linux-riscv64-gnu'];
276
+ if (arch === 'x64') return ['linux-x64-gnu', 'linux-x64-musl'];
277
+ }
278
+ if (platform === 'freebsd' && arch === 'x64') return ['freebsd-x64'];
279
+ return [];
280
+ }
281
+
282
+ function formatErrorMessage(error: unknown): string {
283
+ return error instanceof Error ? error.message : String(error);
284
+ }
285
+
286
+ type KeyringRecoveryOperation = 'read' | 'write' | 'remove';
287
+
288
+ type KeyringRecoveryResponse =
289
+ | { ok: true; found?: boolean; value?: string }
290
+ | { ok: false; error?: string };
291
+
292
+ function isLinuxKeyringRecoveryEnabled(): boolean {
293
+ if (process.env[KEYRING_RECOVERY_DISABLED_ENV] === '1') return false;
294
+ return process.platform === 'linux' || process.env[TEST_LINUX_KEYRING_RECOVERY_ENV] === '1';
295
+ }
296
+
297
+ function shouldAttemptLinuxKeyringRecovery(error: unknown): boolean {
298
+ return isLinuxKeyringRecoveryEnabled()
299
+ && causeChainContains(error, /key\s*(?:has been\s*)?revoked|keyrevoked/i);
300
+ }
301
+
302
+ function runLinuxKeyringRecoveryOperation(operation: KeyringRecoveryOperation, account: string, payload?: string): KeyringRecoveryResponse {
303
+ const keyctl = process.env[KEYRING_RECOVERY_KEYCTL_ENV]?.trim() || 'keyctl';
304
+ const node = process.env[KEYRING_RECOVERY_NODE_ENV]?.trim() || 'node';
305
+ const helper = process.env[KEYRING_RECOVERY_HELPER_ENV]?.trim()
306
+ || fileURLToPath(new URL('./mcp-keyring-helper.cjs', import.meta.url));
307
+ const request = JSON.stringify({ operation, service: AUTH_SECRET_SERVICE, account, payload });
308
+ const result = spawnSync(keyctl, ['session', '-', node, helper], {
309
+ input: `${request}\n`,
310
+ encoding: 'utf8',
311
+ maxBuffer: 1024 * 1024,
312
+ timeout: KEYRING_RECOVERY_TIMEOUT_MS,
313
+ windowsHide: true,
314
+ });
315
+
316
+ if (result.error) {
317
+ throw new Error(`Linux keyring recovery helper could not start: ${result.error.message}`, { cause: result.error });
318
+ }
319
+ if (result.status !== 0) {
320
+ throw new Error(`Linux keyring recovery helper failed with exit code ${result.status ?? 'unknown'}`);
321
+ }
322
+
323
+ let response: unknown;
324
+ try {
325
+ response = JSON.parse(result.stdout.trim()) as unknown;
326
+ } catch (error) {
327
+ throw new Error('Linux keyring recovery helper returned invalid JSON', { cause: error });
328
+ }
329
+ if (typeof response !== 'object' || response === null || typeof (response as { ok?: unknown }).ok !== 'boolean') {
330
+ throw new Error('Linux keyring recovery helper returned an invalid response');
331
+ }
332
+ const typedResponse = response as KeyringRecoveryResponse;
333
+ if (typedResponse.ok === false) {
334
+ throw new Error(typedResponse.error || 'Linux keyring recovery helper failed');
335
+ }
336
+ if (operation === 'read' && typedResponse.found === true && typeof typedResponse.value !== 'string') {
337
+ throw new Error('Linux keyring recovery helper returned an invalid read response');
338
+ }
339
+ return typedResponse;
340
+ }
341
+
342
+ const linuxKeyringRecoveryAuthSecretStore: AuthSecretStore = {
343
+ read(account) {
344
+ const response = runLinuxKeyringRecoveryOperation('read', account);
345
+ return response.ok && response.found === true ? response.value : undefined;
346
+ },
347
+ write(account, payload) {
348
+ runLinuxKeyringRecoveryOperation('write', account, payload);
349
+ },
350
+ remove(account) {
351
+ runLinuxKeyringRecoveryOperation('remove', account);
352
+ },
353
+ };
354
+
355
+ export function loadTestKeyringEntryClass(keyringRequire: KeyringRequire, platform: NodeJS.Platform, arch: NodeJS.Architecture): KeyringEntryConstructor {
356
+ return loadKeyringEntryClass(keyringRequire, platform, arch);
357
+ }
358
+
195
359
  export function getAuthStorageOptions(oauthDir: unknown, cwd = process.cwd()): AuthStorageOptions {
196
360
  const baseDir = resolveConfiguredOAuthDir(oauthDir, cwd);
197
361
  return baseDir ? { baseDir } : {};
@@ -296,8 +460,7 @@ function createChunkManifest(payload: string): AuthEntryChunkManifest {
296
460
  };
297
461
  }
298
462
 
299
- function readChunkedAuthEntry(serverName: string, account: string, manifest: AuthEntryChunkManifest): AuthEntry {
300
- const store = getAuthSecretStore();
463
+ function readChunkedAuthEntry(store: AuthSecretStore, serverName: string, account: string, manifest: AuthEntryChunkManifest): AuthEntry {
301
464
  const chunks = getAuthEntryChunkAccounts(account, manifest).map((chunkAccount) => {
302
465
  try {
303
466
  const chunk = store.read(chunkAccount);
@@ -340,9 +503,8 @@ function removeLegacyAuthEntry(serverName: string, options?: AuthStorageOptions)
340
503
  }
341
504
  }
342
505
 
343
- function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
506
+ function writeSecureAuthEntryToStore(store: AuthSecretStore, serverName: string, entry: AuthEntry): void {
344
507
  const account = getAuthEntryAccount(serverName);
345
- const store = getAuthSecretStore();
346
508
  const payload = JSON.stringify(entry);
347
509
  const previousManifest = readExistingChunkManifest(store, serverName, account);
348
510
  const manifest = payload.length > AUTH_SECRET_CHUNK_SIZE ? createChunkManifest(payload) : undefined;
@@ -371,11 +533,21 @@ function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
371
533
  }
372
534
  }
373
535
 
536
+ function writeSecureAuthEntry(serverName: string, entry: AuthEntry): void {
537
+ try {
538
+ writeSecureAuthEntryToStore(getAuthSecretStore(), serverName, entry);
539
+ } catch (error) {
540
+ if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
541
+ writeSecureAuthEntryToStore(linuxKeyringRecoveryAuthSecretStore, serverName, entry);
542
+ }
543
+ }
544
+
374
545
  /**
375
546
  * Read the auth entry for a server from the OS secure store, importing and
376
547
  * deleting a legacy plaintext entry when present.
377
548
  */
378
- function readAuthEntry(
549
+ function readAuthEntryFromStore(
550
+ store: AuthSecretStore,
379
551
  serverName: string,
380
552
  options?: AuthStorageOptions,
381
553
  behavior: { migrateLegacy?: boolean } = {},
@@ -383,7 +555,7 @@ function readAuthEntry(
383
555
  const account = getAuthEntryAccount(serverName);
384
556
  let payload: string | undefined;
385
557
  try {
386
- payload = getAuthSecretStore().read(account);
558
+ payload = store.read(account);
387
559
  } catch (error) {
388
560
  throw new OAuthCredentialStoreError(
389
561
  `Failed to read OAuth credentials for ${serverName} from the OS secure credential store`,
@@ -395,7 +567,7 @@ function readAuthEntry(
395
567
  if (payload !== undefined) {
396
568
  const manifest = readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
397
569
  const entry = manifest
398
- ? readChunkedAuthEntry(serverName, account, manifest)
570
+ ? readChunkedAuthEntry(store, serverName, account, manifest)
399
571
  : parseAuthEntryPayload(serverName, payload, 'OS secure credential store');
400
572
  removeLegacyAuthEntry(serverName, options);
401
573
  return entry;
@@ -404,11 +576,24 @@ function readAuthEntry(
404
576
  const legacyEntry = readLegacyAuthEntry(serverName, options);
405
577
  if (!legacyEntry) return undefined;
406
578
  if (behavior.migrateLegacy === false) return legacyEntry;
407
- writeSecureAuthEntry(serverName, legacyEntry);
579
+ writeSecureAuthEntryToStore(store, serverName, legacyEntry);
408
580
  removeLegacyAuthEntry(serverName, options);
409
581
  return legacyEntry;
410
582
  }
411
583
 
584
+ function readAuthEntry(
585
+ serverName: string,
586
+ options?: AuthStorageOptions,
587
+ behavior: { migrateLegacy?: boolean } = {},
588
+ ): AuthEntry | undefined {
589
+ try {
590
+ return readAuthEntryFromStore(getAuthSecretStore(), serverName, options, behavior);
591
+ } catch (error) {
592
+ if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
593
+ return readAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName, options, behavior);
594
+ }
595
+ }
596
+
412
597
  /**
413
598
  * Get auth entry for a server.
414
599
  */
@@ -468,9 +653,8 @@ export function saveAuthEntry(serverName: string, entry: AuthEntry, serverUrl?:
468
653
  /**
469
654
  * Remove auth entry for a server.
470
655
  */
471
- export function removeAuthEntry(serverName: string, options?: AuthStorageOptions): void {
656
+ function removeAuthEntryFromStore(store: AuthSecretStore, serverName: string): void {
472
657
  const account = getAuthEntryAccount(serverName);
473
- const store = getAuthSecretStore();
474
658
  try {
475
659
  const payload = store.read(account);
476
660
  const manifest = payload === undefined ? undefined : readChunkManifestFromPayload(serverName, payload, 'OS secure credential store');
@@ -483,6 +667,15 @@ export function removeAuthEntry(serverName: string, options?: AuthStorageOptions
483
667
  error,
484
668
  );
485
669
  }
670
+ }
671
+
672
+ export function removeAuthEntry(serverName: string, options?: AuthStorageOptions): void {
673
+ try {
674
+ removeAuthEntryFromStore(getAuthSecretStore(), serverName);
675
+ } catch (error) {
676
+ if (!shouldAttemptLinuxKeyringRecovery(error)) throw error;
677
+ removeAuthEntryFromStore(linuxKeyringRecoveryAuthSecretStore, serverName);
678
+ }
486
679
  removeLegacyAuthEntry(serverName, options);
487
680
  }
488
681