@akms/mcp-ssh 0.0.1 → 0.0.3

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/lib/index.d.cts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { SFTPWrapper } from 'ssh2';
3
- import { z } from 'zod';
4
3
 
5
4
  /**
6
- * Guard rules applied to every command / file operation on a host.
7
- * Resolved per profile: config `defaults` merged with the profile's own overrides.
5
+ * Guard rules applied to every command / file operation on the host.
6
+ * Resolved at startup: the built-in permissive stance with each `SSH_*` variable applied over it.
8
7
  */
9
8
  interface SshPolicy {
10
9
  /** Blocks write-shaped commands, output redirection, uploads and remote writes. */
@@ -29,9 +28,9 @@ interface SshPolicy {
29
28
  /** Remote path prefixes the file tools may touch. Empty means unrestricted. */
30
29
  allowedPaths: string[];
31
30
  }
32
- /** One registered host: where to connect, how to authenticate, what is permitted. */
31
+ /** The registered host: where to connect, how to authenticate, what is permitted. */
33
32
  interface SshHostProfile {
34
- /** Profile alias — the `target` argument of every tool. */
33
+ /** Alias shown to the model, from `SSH_NAME`; defaults to the hostname. */
35
34
  name: string;
36
35
  host: string;
37
36
  port: number;
@@ -39,8 +38,6 @@ interface SshHostProfile {
39
38
  description?: string;
40
39
  /** Expanded absolute path to a private key file. */
41
40
  privateKeyPath?: string;
42
- /** Private key contents, used when `privateKeyPath` is absent. */
43
- privateKey?: string;
44
41
  passphrase?: string;
45
42
  password?: string;
46
43
  /**
@@ -52,18 +49,7 @@ interface SshHostProfile {
52
49
  defaultCwd?: string;
53
50
  policy: SshPolicy;
54
51
  }
55
- /** Loaded profiles, keyed by alias. */
56
- interface SshMcpConfig {
57
- profiles: Map<string, SshHostProfile>;
58
- /**
59
- * Absolute path the config was read from, or null when it came from inline JSON or
60
- * nothing was found. Only a real file needs protecting from the download tool.
61
- */
62
- sourcePath: string | null;
63
- /** Where the config came from, for tool output — a path, "inline", or "none". */
64
- sourceLabel: string;
65
- }
66
- /** Credential-free view of a profile, safe to hand back to the model. */
52
+ /** Credential-free view of the profile, safe to hand back to the model. */
67
53
  interface SshHostSummary {
68
54
  name: string;
69
55
  host: string;
@@ -325,132 +311,45 @@ declare function uploadFile(connection: SshConnection, localPath: string, remote
325
311
  */
326
312
  declare function downloadFile(connection: SshConnection, remotePath: string, localPath: string): Promise<number>;
327
313
 
328
- interface SshMcpServerOptions {
329
- /** Explicit config path; otherwise `SSH_MCP_CONFIG`, then the home-directory candidates. */
330
- configPath?: string;
331
- }
332
314
  interface SshMcpServerInstance {
333
315
  server: McpServer;
334
316
  sessions: SshSessionManager;
335
- /** Always the config currently in force, including after a reload. */
336
- readonly config: SshMcpConfig;
337
- /**
338
- * Re-reads the config source and applies it.
339
- *
340
- * @throws {Error} If the source is now missing or malformed; the previous config
341
- * stays in force in that case.
342
- */
343
- reloadConfig(): SshMcpConfig;
317
+ /** The host read from the `SSH_*` variables at startup, or null when none is set. */
318
+ readonly profile: SshHostProfile | null;
344
319
  /** Stops the sweeper and closes every open session. Call before the process exits. */
345
320
  shutdown(): void;
346
321
  }
347
322
  /**
348
- * Builds the MCP server: loads host profiles, wires the session manager, registers tools.
323
+ * Builds the MCP server: reads the host from the environment, wires the session manager,
324
+ * registers tools.
349
325
  *
350
326
  * Transport is deliberately left to the caller — `cli.ts` attaches stdio, tests can attach
351
327
  * an in-memory pair.
352
328
  *
353
- * @throws {Error} If a config file exists but is malformed (a bad policy field must not
329
+ * @throws {Error} If the `SSH_*` variables are malformed (a bad policy value must not
354
330
  * silently degrade into "no guard").
355
331
  */
356
- declare function createSshMcpServer(options?: SshMcpServerOptions): SshMcpServerInstance;
332
+ declare function createSshMcpServer(): SshMcpServerInstance;
357
333
 
358
334
  /**
359
- * Loads host profiles from disk.
335
+ * Builds the host profile from the `SSH_*` variables, or null when `SSH_HOST` is unset.
360
336
  *
361
- * A missing config is not an error — the server still starts and reports "no profiles"
362
- * through the tools, which is a far clearer signal to the model than a dead server.
363
- * A config that exists but is malformed *is* an error: silently ignoring it would let a
364
- * typo in a policy field quietly drop a `readonly` guard.
337
+ * One MCP server entry fronts one host, so every setting is a plain variable — no JSON
338
+ * inside JSON, no escaped quotes, and a typo shows up on the line it is on. A malformed
339
+ * value aborts startup rather than being ignored, because silently dropping `SSH_READONLY`
340
+ * would leave a host the operator believed was read-only fully writable.
365
341
  *
366
- * @throws {Error} If the resolved path is missing, unreadable, not JSON, or fails validation.
367
- */
368
- declare function loadConfig(explicitPath?: string): SshMcpConfig;
369
- /** Strips every secret so a profile can be described back to the model. */
370
- declare function toHostSummary(profile: SshHostProfile): SshHostSummary;
371
-
372
- /**
373
- * Policy knobs, all optional — unset fields fall back to `defaults`, then to the built-ins.
342
+ * Flow:
343
+ * 1) Guard: no `SSH_HOST` means nothing is registered — the server still starts and says so
344
+ * 2) Resolve the policy over the built-in permissive stance; each variable is opt-in restriction
345
+ * 3) Assemble the connection fields
346
+ * 4) Expand the private key path and check it exists
374
347
  *
375
- * Strict on purpose. A permissive object *strips* unknown keys, so `readOnly: true`
376
- * (capital O) parsed successfully and silently vanished, leaving a host the operator
377
- * believed was read-only fully writable. An unknown key must fail startup instead.
348
+ * @throws {Error} If `SSH_HOST` is set without `SSH_USER`, or a typed value is malformed.
378
349
  */
379
- declare const POLICY_INPUT_SCHEMA: z.ZodObject<{
380
- readonly: z.ZodOptional<z.ZodBoolean>;
381
- allowSudo: z.ZodOptional<z.ZodBoolean>;
382
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
383
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
384
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
385
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
386
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
387
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
388
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
389
- }, z.core.$strict>;
390
- /** One `hosts` entry: connection fields plus the same policy knobs as an override. */
391
- declare const HOST_INPUT_SCHEMA: z.ZodObject<{
392
- readonly: z.ZodOptional<z.ZodBoolean>;
393
- allowSudo: z.ZodOptional<z.ZodBoolean>;
394
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
395
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
396
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
397
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
398
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
399
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
400
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
401
- host: z.ZodString;
402
- port: z.ZodOptional<z.ZodNumber>;
403
- username: z.ZodString;
404
- description: z.ZodOptional<z.ZodString>;
405
- privateKeyPath: z.ZodOptional<z.ZodString>;
406
- privateKey: z.ZodOptional<z.ZodString>;
407
- passphrase: z.ZodOptional<z.ZodString>;
408
- passphraseEnv: z.ZodOptional<z.ZodString>;
409
- password: z.ZodOptional<z.ZodString>;
410
- passwordEnv: z.ZodOptional<z.ZodString>;
411
- hostKeyFingerprint: z.ZodOptional<z.ZodString>;
412
- defaultCwd: z.ZodOptional<z.ZodString>;
413
- }, z.core.$strict>;
414
- declare const CONFIG_INPUT_SCHEMA: z.ZodObject<{
415
- $schema: z.ZodOptional<z.ZodString>;
416
- defaults: z.ZodOptional<z.ZodObject<{
417
- readonly: z.ZodOptional<z.ZodBoolean>;
418
- allowSudo: z.ZodOptional<z.ZodBoolean>;
419
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
420
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
421
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
422
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
423
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
424
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
425
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
426
- }, z.core.$strict>>;
427
- hosts: z.ZodRecord<z.ZodString, z.ZodObject<{
428
- readonly: z.ZodOptional<z.ZodBoolean>;
429
- allowSudo: z.ZodOptional<z.ZodBoolean>;
430
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
431
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
432
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
433
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
434
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
435
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
436
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
437
- host: z.ZodString;
438
- port: z.ZodOptional<z.ZodNumber>;
439
- username: z.ZodString;
440
- description: z.ZodOptional<z.ZodString>;
441
- privateKeyPath: z.ZodOptional<z.ZodString>;
442
- privateKey: z.ZodOptional<z.ZodString>;
443
- passphrase: z.ZodOptional<z.ZodString>;
444
- passphraseEnv: z.ZodOptional<z.ZodString>;
445
- password: z.ZodOptional<z.ZodString>;
446
- passwordEnv: z.ZodOptional<z.ZodString>;
447
- hostKeyFingerprint: z.ZodOptional<z.ZodString>;
448
- defaultCwd: z.ZodOptional<z.ZodString>;
449
- }, z.core.$strict>>;
450
- }, z.core.$strict>;
451
- type PolicyInput = z.infer<typeof POLICY_INPUT_SCHEMA>;
452
- type HostInput = z.infer<typeof HOST_INPUT_SCHEMA>;
453
- type ConfigInput = z.infer<typeof CONFIG_INPUT_SCHEMA>;
350
+ declare function loadHostProfile(): SshHostProfile | null;
351
+ /** Strips every secret so the profile can be described back to the model. */
352
+ declare function toHostSummary(profile: SshHostProfile): SshHostSummary;
454
353
 
455
354
  /**
456
355
  * Rewrites shell constructs that change how the text tokenizes but not what it runs,
@@ -546,7 +445,8 @@ declare function inspectRemotePath(remotePath: string, policy: SshPolicy, access
546
445
  * define the guards themselves (download).
547
446
  *
548
447
  * @param resolvedLocalPath Absolute path, already `~`-expanded.
549
- * @param extraProtectedPaths Runtime-known paths to protect, such as the loaded config file.
448
+ * @param extraProtectedPaths Runtime-known paths to protect on top of the built-in rules,
449
+ * for a host embedding this server as a library.
550
450
  */
551
451
  declare function inspectLocalPath(resolvedLocalPath: string, access: RemotePathAccess, extraProtectedPaths?: string[]): GuardVerdict;
552
452
  /**
@@ -571,15 +471,11 @@ declare class GuardRejectionError extends Error {
571
471
  constructor(verdict: GuardVerdict);
572
472
  }
573
473
 
574
- /** Everything the tool handlers share: the loaded profiles and the live sessions. */
474
+ /** Everything the tool handlers share: the configured host and the live sessions. */
575
475
  interface ToolContext {
576
- config: SshMcpConfig;
476
+ /** The host from the `SSH_*` variables, or null when none is registered. */
477
+ profile: SshHostProfile | null;
577
478
  sessions: SshSessionManager;
578
- /**
579
- * Re-reads the config source and swaps it in, so `context.config` is always current.
580
- * Implemented by the server, which owns the source path.
581
- */
582
- reloadConfig(): SshMcpConfig;
583
479
  }
584
480
 
585
481
  /** Registers every tool this server exposes, in the order the model should discover them. */
@@ -596,11 +492,46 @@ interface Logger {
596
492
  }
597
493
  declare const $logger: Logger;
598
494
 
599
- /** Env var holding an explicit config file path. */
600
- declare const CONFIG_PATH_ENV = "SSH_MCP_CONFIG";
601
495
  /** Env var overriding the stderr log level ("debug" | "info" | "warn" | "error" | "silent"). */
602
496
  declare const LOG_LEVEL_ENV = "SSH_MCP_LOG_LEVEL";
603
- declare const SERVER_NAME = "akms-ssh";
604
- declare const SERVER_VERSION = "0.0.1";
497
+ declare const SERVER_NAME = "akms-mcp-ssh";
498
+ declare const SERVER_VERSION = "0.0.3";
499
+
500
+ /**
501
+ * Environment variables that define the host, one MCP server entry per host.
502
+ *
503
+ * This is the only way to configure the server. Each variable is a plain value, so an
504
+ * MCP client's `env` block reads like a connection form — no JSON inside JSON, no
505
+ * escaped quotes, and a typo shows up on the line it is on.
506
+ */
507
+ declare const SINGLE_HOST_ENV: {
508
+ /** Presence of this variable is what registers a host at all. */
509
+ readonly HOST: "SSH_HOST";
510
+ readonly PORT: "SSH_PORT";
511
+ readonly USER: "SSH_USER";
512
+ /** Path to a private key file. */
513
+ readonly KEY: "SSH_KEY";
514
+ readonly PASSPHRASE: "SSH_PASSPHRASE";
515
+ readonly PASSWORD: "SSH_PASSWORD";
516
+ /** Pinned host key, `SHA256:…`. */
517
+ readonly HOST_KEY: "SSH_HOST_KEY";
518
+ /** Alias shown to the model; defaults to the hostname. */
519
+ readonly NAME: "SSH_NAME";
520
+ readonly DESCRIPTION: "SSH_DESCRIPTION";
521
+ /** Directory new sessions start in. */
522
+ readonly CWD: "SSH_CWD";
523
+ readonly READONLY: "SSH_READONLY";
524
+ readonly ALLOW_SUDO: "SSH_ALLOW_SUDO";
525
+ /** Comma-separated binary names. */
526
+ readonly ALLOW_COMMANDS: "SSH_ALLOW_COMMANDS";
527
+ /** Comma-separated regex sources. */
528
+ readonly DENY_PATTERNS: "SSH_DENY_PATTERNS";
529
+ /** Comma-separated absolute path prefixes. */
530
+ readonly ALLOWED_PATHS: "SSH_ALLOWED_PATHS";
531
+ readonly EXEC_TIMEOUT_MS: "SSH_EXEC_TIMEOUT_MS";
532
+ readonly CONNECT_TIMEOUT_MS: "SSH_CONNECT_TIMEOUT_MS";
533
+ readonly MAX_OUTPUT_CHARACTERS: "SSH_MAX_OUTPUT";
534
+ readonly MAX_READ_FILE_BYTES: "SSH_MAX_READ_BYTES";
535
+ };
605
536
 
606
- export { $logger, CONFIG_INPUT_SCHEMA, CONFIG_PATH_ENV, type ConfigInput, GuardRejectionError, type GuardVerdict, HOST_INPUT_SCHEMA, type HostInput, LOG_LEVEL_ENV, type LogLevel, type Logger, POLICY_INPUT_SCHEMA, type PolicyInput, type RemoteDirectoryListing, type RemoteFileContent, type RemotePathAccess, SERVER_NAME, SERVER_VERSION, type SegmentAnalysis, SshConnection, type SshExecOptions, type SshExecResult, type SshHostProfile, type SshHostSummary, type SshMcpConfig, type SshMcpServerInstance, type SshMcpServerOptions, type SshPolicy, type SshRemoteEntry, SshSession, type SshSessionInfo, SshSessionManager, type ToolContext, analyzeSegment, canonicalizeCommand, createSshMcpServer, downloadFile, extractSubstitutions, inspectCommand, inspectLocalPath, inspectRemotePath, inspectWorkingDirectory, listRemoteDirectory, loadConfig, normalizeCommandText, readRemoteFile, registerAllTools, splitCommandSegments, stripQuotedText, toHostSummary, uploadFile, writeRemoteFile };
537
+ export { $logger, GuardRejectionError, type GuardVerdict, LOG_LEVEL_ENV, type LogLevel, type Logger, type RemoteDirectoryListing, type RemoteFileContent, type RemotePathAccess, SERVER_NAME, SERVER_VERSION, SINGLE_HOST_ENV, type SegmentAnalysis, SshConnection, type SshExecOptions, type SshExecResult, type SshHostProfile, type SshHostSummary, type SshMcpServerInstance, type SshPolicy, type SshRemoteEntry, SshSession, type SshSessionInfo, SshSessionManager, type ToolContext, analyzeSegment, canonicalizeCommand, createSshMcpServer, downloadFile, extractSubstitutions, inspectCommand, inspectLocalPath, inspectRemotePath, inspectWorkingDirectory, listRemoteDirectory, loadHostProfile, normalizeCommandText, readRemoteFile, registerAllTools, splitCommandSegments, stripQuotedText, toHostSummary, uploadFile, writeRemoteFile };
package/lib/index.d.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { SFTPWrapper } from 'ssh2';
3
- import { z } from 'zod';
4
3
 
5
4
  /**
6
- * Guard rules applied to every command / file operation on a host.
7
- * Resolved per profile: config `defaults` merged with the profile's own overrides.
5
+ * Guard rules applied to every command / file operation on the host.
6
+ * Resolved at startup: the built-in permissive stance with each `SSH_*` variable applied over it.
8
7
  */
9
8
  interface SshPolicy {
10
9
  /** Blocks write-shaped commands, output redirection, uploads and remote writes. */
@@ -29,9 +28,9 @@ interface SshPolicy {
29
28
  /** Remote path prefixes the file tools may touch. Empty means unrestricted. */
30
29
  allowedPaths: string[];
31
30
  }
32
- /** One registered host: where to connect, how to authenticate, what is permitted. */
31
+ /** The registered host: where to connect, how to authenticate, what is permitted. */
33
32
  interface SshHostProfile {
34
- /** Profile alias — the `target` argument of every tool. */
33
+ /** Alias shown to the model, from `SSH_NAME`; defaults to the hostname. */
35
34
  name: string;
36
35
  host: string;
37
36
  port: number;
@@ -39,8 +38,6 @@ interface SshHostProfile {
39
38
  description?: string;
40
39
  /** Expanded absolute path to a private key file. */
41
40
  privateKeyPath?: string;
42
- /** Private key contents, used when `privateKeyPath` is absent. */
43
- privateKey?: string;
44
41
  passphrase?: string;
45
42
  password?: string;
46
43
  /**
@@ -52,18 +49,7 @@ interface SshHostProfile {
52
49
  defaultCwd?: string;
53
50
  policy: SshPolicy;
54
51
  }
55
- /** Loaded profiles, keyed by alias. */
56
- interface SshMcpConfig {
57
- profiles: Map<string, SshHostProfile>;
58
- /**
59
- * Absolute path the config was read from, or null when it came from inline JSON or
60
- * nothing was found. Only a real file needs protecting from the download tool.
61
- */
62
- sourcePath: string | null;
63
- /** Where the config came from, for tool output — a path, "inline", or "none". */
64
- sourceLabel: string;
65
- }
66
- /** Credential-free view of a profile, safe to hand back to the model. */
52
+ /** Credential-free view of the profile, safe to hand back to the model. */
67
53
  interface SshHostSummary {
68
54
  name: string;
69
55
  host: string;
@@ -325,132 +311,45 @@ declare function uploadFile(connection: SshConnection, localPath: string, remote
325
311
  */
326
312
  declare function downloadFile(connection: SshConnection, remotePath: string, localPath: string): Promise<number>;
327
313
 
328
- interface SshMcpServerOptions {
329
- /** Explicit config path; otherwise `SSH_MCP_CONFIG`, then the home-directory candidates. */
330
- configPath?: string;
331
- }
332
314
  interface SshMcpServerInstance {
333
315
  server: McpServer;
334
316
  sessions: SshSessionManager;
335
- /** Always the config currently in force, including after a reload. */
336
- readonly config: SshMcpConfig;
337
- /**
338
- * Re-reads the config source and applies it.
339
- *
340
- * @throws {Error} If the source is now missing or malformed; the previous config
341
- * stays in force in that case.
342
- */
343
- reloadConfig(): SshMcpConfig;
317
+ /** The host read from the `SSH_*` variables at startup, or null when none is set. */
318
+ readonly profile: SshHostProfile | null;
344
319
  /** Stops the sweeper and closes every open session. Call before the process exits. */
345
320
  shutdown(): void;
346
321
  }
347
322
  /**
348
- * Builds the MCP server: loads host profiles, wires the session manager, registers tools.
323
+ * Builds the MCP server: reads the host from the environment, wires the session manager,
324
+ * registers tools.
349
325
  *
350
326
  * Transport is deliberately left to the caller — `cli.ts` attaches stdio, tests can attach
351
327
  * an in-memory pair.
352
328
  *
353
- * @throws {Error} If a config file exists but is malformed (a bad policy field must not
329
+ * @throws {Error} If the `SSH_*` variables are malformed (a bad policy value must not
354
330
  * silently degrade into "no guard").
355
331
  */
356
- declare function createSshMcpServer(options?: SshMcpServerOptions): SshMcpServerInstance;
332
+ declare function createSshMcpServer(): SshMcpServerInstance;
357
333
 
358
334
  /**
359
- * Loads host profiles from disk.
335
+ * Builds the host profile from the `SSH_*` variables, or null when `SSH_HOST` is unset.
360
336
  *
361
- * A missing config is not an error — the server still starts and reports "no profiles"
362
- * through the tools, which is a far clearer signal to the model than a dead server.
363
- * A config that exists but is malformed *is* an error: silently ignoring it would let a
364
- * typo in a policy field quietly drop a `readonly` guard.
337
+ * One MCP server entry fronts one host, so every setting is a plain variable — no JSON
338
+ * inside JSON, no escaped quotes, and a typo shows up on the line it is on. A malformed
339
+ * value aborts startup rather than being ignored, because silently dropping `SSH_READONLY`
340
+ * would leave a host the operator believed was read-only fully writable.
365
341
  *
366
- * @throws {Error} If the resolved path is missing, unreadable, not JSON, or fails validation.
367
- */
368
- declare function loadConfig(explicitPath?: string): SshMcpConfig;
369
- /** Strips every secret so a profile can be described back to the model. */
370
- declare function toHostSummary(profile: SshHostProfile): SshHostSummary;
371
-
372
- /**
373
- * Policy knobs, all optional — unset fields fall back to `defaults`, then to the built-ins.
342
+ * Flow:
343
+ * 1) Guard: no `SSH_HOST` means nothing is registered — the server still starts and says so
344
+ * 2) Resolve the policy over the built-in permissive stance; each variable is opt-in restriction
345
+ * 3) Assemble the connection fields
346
+ * 4) Expand the private key path and check it exists
374
347
  *
375
- * Strict on purpose. A permissive object *strips* unknown keys, so `readOnly: true`
376
- * (capital O) parsed successfully and silently vanished, leaving a host the operator
377
- * believed was read-only fully writable. An unknown key must fail startup instead.
348
+ * @throws {Error} If `SSH_HOST` is set without `SSH_USER`, or a typed value is malformed.
378
349
  */
379
- declare const POLICY_INPUT_SCHEMA: z.ZodObject<{
380
- readonly: z.ZodOptional<z.ZodBoolean>;
381
- allowSudo: z.ZodOptional<z.ZodBoolean>;
382
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
383
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
384
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
385
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
386
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
387
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
388
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
389
- }, z.core.$strict>;
390
- /** One `hosts` entry: connection fields plus the same policy knobs as an override. */
391
- declare const HOST_INPUT_SCHEMA: z.ZodObject<{
392
- readonly: z.ZodOptional<z.ZodBoolean>;
393
- allowSudo: z.ZodOptional<z.ZodBoolean>;
394
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
395
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
396
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
397
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
398
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
399
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
400
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
401
- host: z.ZodString;
402
- port: z.ZodOptional<z.ZodNumber>;
403
- username: z.ZodString;
404
- description: z.ZodOptional<z.ZodString>;
405
- privateKeyPath: z.ZodOptional<z.ZodString>;
406
- privateKey: z.ZodOptional<z.ZodString>;
407
- passphrase: z.ZodOptional<z.ZodString>;
408
- passphraseEnv: z.ZodOptional<z.ZodString>;
409
- password: z.ZodOptional<z.ZodString>;
410
- passwordEnv: z.ZodOptional<z.ZodString>;
411
- hostKeyFingerprint: z.ZodOptional<z.ZodString>;
412
- defaultCwd: z.ZodOptional<z.ZodString>;
413
- }, z.core.$strict>;
414
- declare const CONFIG_INPUT_SCHEMA: z.ZodObject<{
415
- $schema: z.ZodOptional<z.ZodString>;
416
- defaults: z.ZodOptional<z.ZodObject<{
417
- readonly: z.ZodOptional<z.ZodBoolean>;
418
- allowSudo: z.ZodOptional<z.ZodBoolean>;
419
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
420
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
421
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
422
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
423
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
424
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
425
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
426
- }, z.core.$strict>>;
427
- hosts: z.ZodRecord<z.ZodString, z.ZodObject<{
428
- readonly: z.ZodOptional<z.ZodBoolean>;
429
- allowSudo: z.ZodOptional<z.ZodBoolean>;
430
- execTimeoutMs: z.ZodOptional<z.ZodNumber>;
431
- connectTimeoutMs: z.ZodOptional<z.ZodNumber>;
432
- maxOutputCharacters: z.ZodOptional<z.ZodNumber>;
433
- maxReadFileBytes: z.ZodOptional<z.ZodNumber>;
434
- allowCommands: z.ZodOptional<z.ZodArray<z.ZodString>>;
435
- denyPatterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
436
- allowedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
437
- host: z.ZodString;
438
- port: z.ZodOptional<z.ZodNumber>;
439
- username: z.ZodString;
440
- description: z.ZodOptional<z.ZodString>;
441
- privateKeyPath: z.ZodOptional<z.ZodString>;
442
- privateKey: z.ZodOptional<z.ZodString>;
443
- passphrase: z.ZodOptional<z.ZodString>;
444
- passphraseEnv: z.ZodOptional<z.ZodString>;
445
- password: z.ZodOptional<z.ZodString>;
446
- passwordEnv: z.ZodOptional<z.ZodString>;
447
- hostKeyFingerprint: z.ZodOptional<z.ZodString>;
448
- defaultCwd: z.ZodOptional<z.ZodString>;
449
- }, z.core.$strict>>;
450
- }, z.core.$strict>;
451
- type PolicyInput = z.infer<typeof POLICY_INPUT_SCHEMA>;
452
- type HostInput = z.infer<typeof HOST_INPUT_SCHEMA>;
453
- type ConfigInput = z.infer<typeof CONFIG_INPUT_SCHEMA>;
350
+ declare function loadHostProfile(): SshHostProfile | null;
351
+ /** Strips every secret so the profile can be described back to the model. */
352
+ declare function toHostSummary(profile: SshHostProfile): SshHostSummary;
454
353
 
455
354
  /**
456
355
  * Rewrites shell constructs that change how the text tokenizes but not what it runs,
@@ -546,7 +445,8 @@ declare function inspectRemotePath(remotePath: string, policy: SshPolicy, access
546
445
  * define the guards themselves (download).
547
446
  *
548
447
  * @param resolvedLocalPath Absolute path, already `~`-expanded.
549
- * @param extraProtectedPaths Runtime-known paths to protect, such as the loaded config file.
448
+ * @param extraProtectedPaths Runtime-known paths to protect on top of the built-in rules,
449
+ * for a host embedding this server as a library.
550
450
  */
551
451
  declare function inspectLocalPath(resolvedLocalPath: string, access: RemotePathAccess, extraProtectedPaths?: string[]): GuardVerdict;
552
452
  /**
@@ -571,15 +471,11 @@ declare class GuardRejectionError extends Error {
571
471
  constructor(verdict: GuardVerdict);
572
472
  }
573
473
 
574
- /** Everything the tool handlers share: the loaded profiles and the live sessions. */
474
+ /** Everything the tool handlers share: the configured host and the live sessions. */
575
475
  interface ToolContext {
576
- config: SshMcpConfig;
476
+ /** The host from the `SSH_*` variables, or null when none is registered. */
477
+ profile: SshHostProfile | null;
577
478
  sessions: SshSessionManager;
578
- /**
579
- * Re-reads the config source and swaps it in, so `context.config` is always current.
580
- * Implemented by the server, which owns the source path.
581
- */
582
- reloadConfig(): SshMcpConfig;
583
479
  }
584
480
 
585
481
  /** Registers every tool this server exposes, in the order the model should discover them. */
@@ -596,11 +492,46 @@ interface Logger {
596
492
  }
597
493
  declare const $logger: Logger;
598
494
 
599
- /** Env var holding an explicit config file path. */
600
- declare const CONFIG_PATH_ENV = "SSH_MCP_CONFIG";
601
495
  /** Env var overriding the stderr log level ("debug" | "info" | "warn" | "error" | "silent"). */
602
496
  declare const LOG_LEVEL_ENV = "SSH_MCP_LOG_LEVEL";
603
- declare const SERVER_NAME = "akms-ssh";
604
- declare const SERVER_VERSION = "0.0.1";
497
+ declare const SERVER_NAME = "akms-mcp-ssh";
498
+ declare const SERVER_VERSION = "0.0.3";
499
+
500
+ /**
501
+ * Environment variables that define the host, one MCP server entry per host.
502
+ *
503
+ * This is the only way to configure the server. Each variable is a plain value, so an
504
+ * MCP client's `env` block reads like a connection form — no JSON inside JSON, no
505
+ * escaped quotes, and a typo shows up on the line it is on.
506
+ */
507
+ declare const SINGLE_HOST_ENV: {
508
+ /** Presence of this variable is what registers a host at all. */
509
+ readonly HOST: "SSH_HOST";
510
+ readonly PORT: "SSH_PORT";
511
+ readonly USER: "SSH_USER";
512
+ /** Path to a private key file. */
513
+ readonly KEY: "SSH_KEY";
514
+ readonly PASSPHRASE: "SSH_PASSPHRASE";
515
+ readonly PASSWORD: "SSH_PASSWORD";
516
+ /** Pinned host key, `SHA256:…`. */
517
+ readonly HOST_KEY: "SSH_HOST_KEY";
518
+ /** Alias shown to the model; defaults to the hostname. */
519
+ readonly NAME: "SSH_NAME";
520
+ readonly DESCRIPTION: "SSH_DESCRIPTION";
521
+ /** Directory new sessions start in. */
522
+ readonly CWD: "SSH_CWD";
523
+ readonly READONLY: "SSH_READONLY";
524
+ readonly ALLOW_SUDO: "SSH_ALLOW_SUDO";
525
+ /** Comma-separated binary names. */
526
+ readonly ALLOW_COMMANDS: "SSH_ALLOW_COMMANDS";
527
+ /** Comma-separated regex sources. */
528
+ readonly DENY_PATTERNS: "SSH_DENY_PATTERNS";
529
+ /** Comma-separated absolute path prefixes. */
530
+ readonly ALLOWED_PATHS: "SSH_ALLOWED_PATHS";
531
+ readonly EXEC_TIMEOUT_MS: "SSH_EXEC_TIMEOUT_MS";
532
+ readonly CONNECT_TIMEOUT_MS: "SSH_CONNECT_TIMEOUT_MS";
533
+ readonly MAX_OUTPUT_CHARACTERS: "SSH_MAX_OUTPUT";
534
+ readonly MAX_READ_FILE_BYTES: "SSH_MAX_READ_BYTES";
535
+ };
605
536
 
606
- export { $logger, CONFIG_INPUT_SCHEMA, CONFIG_PATH_ENV, type ConfigInput, GuardRejectionError, type GuardVerdict, HOST_INPUT_SCHEMA, type HostInput, LOG_LEVEL_ENV, type LogLevel, type Logger, POLICY_INPUT_SCHEMA, type PolicyInput, type RemoteDirectoryListing, type RemoteFileContent, type RemotePathAccess, SERVER_NAME, SERVER_VERSION, type SegmentAnalysis, SshConnection, type SshExecOptions, type SshExecResult, type SshHostProfile, type SshHostSummary, type SshMcpConfig, type SshMcpServerInstance, type SshMcpServerOptions, type SshPolicy, type SshRemoteEntry, SshSession, type SshSessionInfo, SshSessionManager, type ToolContext, analyzeSegment, canonicalizeCommand, createSshMcpServer, downloadFile, extractSubstitutions, inspectCommand, inspectLocalPath, inspectRemotePath, inspectWorkingDirectory, listRemoteDirectory, loadConfig, normalizeCommandText, readRemoteFile, registerAllTools, splitCommandSegments, stripQuotedText, toHostSummary, uploadFile, writeRemoteFile };
537
+ export { $logger, GuardRejectionError, type GuardVerdict, LOG_LEVEL_ENV, type LogLevel, type Logger, type RemoteDirectoryListing, type RemoteFileContent, type RemotePathAccess, SERVER_NAME, SERVER_VERSION, SINGLE_HOST_ENV, type SegmentAnalysis, SshConnection, type SshExecOptions, type SshExecResult, type SshHostProfile, type SshHostSummary, type SshMcpServerInstance, type SshPolicy, type SshRemoteEntry, SshSession, type SshSessionInfo, SshSessionManager, type ToolContext, analyzeSegment, canonicalizeCommand, createSshMcpServer, downloadFile, extractSubstitutions, inspectCommand, inspectLocalPath, inspectRemotePath, inspectWorkingDirectory, listRemoteDirectory, loadHostProfile, normalizeCommandText, readRemoteFile, registerAllTools, splitCommandSegments, stripQuotedText, toHostSummary, uploadFile, writeRemoteFile };