Skip to main content
Glama
restforge

@restforge-dev/mcp-server

Official
by restforge

Update Database Connection Env

setup_update_env
Idempotent

Partially update a configuration env file by merging specified key-value pairs, preserving comments, blank lines, and unmodified parameters.

Instructions

Apply a partial update to config/db-connection.env. Accepts an arbitrary set of key/value pairs and merges them into the existing file, preserving comments, blank lines, and untouched parameters.

USE WHEN:

  • Toggling individual feature flags (e.g. LIVE_SYNC_ENABLED, REDIS_ENABLED, KAFKA_ENABLED)

  • Adjusting one or two values without restating the whole core connection

  • Adding a new optional parameter that is not in the template

DO NOT USE FOR:

  • Generating the initial config skeleton -> use 'setup_init_config'

  • Bulk write of license + DB credentials -> use 'setup_write_env'

  • Validating connection -> use 'setup_validate_config'

Behavior: read existing file, replace matching keys (preserving inline comments), append non-existing keys at the bottom, write back. Values may be string, number, or boolean (booleans serialize as 'true'/'false'). Values containing spaces, '=' or '#' are auto-quoted.

PRESENTATION GUIDANCE:

  • Match the user's language. If the user writes in Indonesian, respond in Indonesian.

  • Never mention internal tool names in the reply to the user. Describe actions by what they do (e.g. "set up the initial config", "fill in the credentials", "validate the connection").

  • Speak in plain language. Summarise the result by counting and naming the changed keys; do not paste the full diff block unless the user explicitly asks.

  • Do not echo sensitive values (license keys, passwords) into chat even when they appear masked in the response. Confirm presence and length only.

  • When a precondition is not met (e.g. config file is missing), frame it as a question or next-step suggestion rather than an error.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
cwdYesAbsolute path of the project folder
configFileNoConfig file name in the config/ folder. Default: db-connection.envdb-connection.env
fieldsYesKey-value map of parameters to update or add. Example: { "DB_PORT": 5433, "KAFKA_ENABLED": true }

Implementation Reference

  • The registerSetupUpdateEnv function registers the 'setup_update_env' tool with the MCP server. Lines 14-57 define the tool metadata, description, and input schema. Lines 58-196 contain the async handler that: (1) resolves the config file path, (2) checks file existence as a precondition, (3) reads the existing file, (4) parses, merges, and serializes env entries using helpers from env-parser.ts, (5) writes the updated file, and (6) returns a structured success/error response.
    export function registerSetupUpdateEnv(server: McpServer): void {
      server.registerTool(
        'setup_update_env',
        {
          title: 'Update Database Connection Env',
          description: `Apply a partial update to config/db-connection.env. Accepts an arbitrary set of key/value pairs and merges them into the existing file, preserving comments, blank lines, and untouched parameters.
    
    USE WHEN:
    - Toggling individual feature flags (e.g. LIVE_SYNC_ENABLED, REDIS_ENABLED, KAFKA_ENABLED)
    - Adjusting one or two values without restating the whole core connection
    - Adding a new optional parameter that is not in the template
    
    DO NOT USE FOR:
    - Generating the initial config skeleton -> use 'setup_init_config'
    - Bulk write of license + DB credentials -> use 'setup_write_env'
    - Validating connection -> use 'setup_validate_config'
    
    Behavior: read existing file, replace matching keys (preserving inline comments), append non-existing keys at the bottom, write back. Values may be string, number, or boolean (booleans serialize as 'true'/'false'). Values containing spaces, '=' or '#' are auto-quoted.
    
    PRESENTATION GUIDANCE:
    - Match the user's language. If the user writes in Indonesian, respond in Indonesian.
    - Never mention internal tool names in the reply to the user. Describe actions by what they do (e.g. "set up the initial config", "fill in the credentials", "validate the connection").
    - Speak in plain language. Summarise the result by counting and naming the changed keys; do not paste the full diff block unless the user explicitly asks.
    - Do not echo sensitive values (license keys, passwords) into chat even when they appear masked in the response. Confirm presence and length only.
    - When a precondition is not met (e.g. config file is missing), frame it as a question or next-step suggestion rather than an error.`,
          inputSchema: {
            cwd: z.string().min(1).describe('Absolute path of the project folder'),
            configFile: z
              .string()
              .default('db-connection.env')
              .describe('Config file name in the config/ folder. Default: db-connection.env'),
            fields: z
              .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
              .refine((obj) => Object.keys(obj).length > 0, {
                message: 'fields must contain at least one key',
              })
              .describe('Key-value map of parameters to update or add. Example: { "DB_PORT": 5433, "KAFKA_ENABLED": true }'),
          },
          annotations: {
            title: 'Update Env Config',
            readOnlyHint: false,
            idempotentHint: true,
          },
        },
        async ({ cwd, configFile, fields }) => {
          const projectCwd = resolve(cwd);
          const envPath = join(projectCwd, 'config', configFile);
    
          // Precondition check: the config file must exist before it can be updated.
          // Treated as a non-error precondition per the authoring guide §3.4.
          try {
            await access(envPath);
          } catch {
            return {
              content: [
                {
                  type: 'text',
                  text: `Precondition not met: the configuration file does not exist yet.
    
    Project path: ${projectCwd}
    Expected file: ${envPath}
    
    For the assistant:
    - The user is trying to update a configuration that has not been created yet.
    - Suggest generating the initial RESTForge configuration first, then retry the update.
    - When explaining to the user, say something like "the configuration file isn't there yet — should I set up the initial config first?". Do not mention internal tool names.`,
                },
              ],
              isError: false,
            };
          }
    
          // Real I/O failure on read: surface as error per §3.4.
          let existingContent: string;
          try {
            existingContent = await readFile(envPath, 'utf-8');
          } catch (error: unknown) {
            const msg = error instanceof Error ? error.message : String(error);
            return {
              content: [
                {
                  type: 'text',
                  text: `Failed to read the configuration file before applying the update.
    
    Project path: ${projectCwd}
    File: ${envPath}
    Reason: ${msg}
    
    For the assistant:
    - Tell the user that the configuration file exists but could not be read.
    - Summarise the likely cause (permissions, file lock, encoding) in plain language; do not paste the raw error unless the user explicitly asks.
    - Offer to retry once the underlying issue is resolved. Do not mention internal tool names.`,
                },
              ],
              isError: true,
            };
          }
    
          const existingEntries = parseEnvFile(existingContent);
          const merged = mergeEnvEntries(existingEntries, fields as Record<string, EnvFieldValue>);
          const newContent = serializeEnvFile(merged.entries);
    
          // Real I/O failure on write: surface as error per §3.4.
          try {
            await writeFile(envPath, newContent, 'utf-8');
          } catch (error: unknown) {
            const msg = error instanceof Error ? error.message : String(error);
            return {
              content: [
                {
                  type: 'text',
                  text: `Failed to write the updated configuration to disk.
    
    Project path: ${projectCwd}
    File: ${envPath}
    Reason: ${msg}
    
    For the assistant:
    - Tell the user that the update could not be persisted.
    - Summarise the likely cause (permissions, disk full, file lock) in plain language; do not paste the raw error unless the user explicitly asks.
    - The original file is unchanged. Offer to retry once the underlying issue is resolved. Do not mention internal tool names.`,
                },
              ],
              isError: true,
            };
          }
    
          // Success: labeled facts + fenced change summary per §3.5.
          const formatChange = (key: string, before: string, after: string): string => {
            if (isSensitiveKey(key)) {
              return `  - ${key}: ${maskValue(before)} -> ${maskValue(after)}`;
            }
            return `  - ${key}: ${before} -> ${after}`;
          };
    
          const formatAdd = (key: string, value: string): string => {
            const v = isSensitiveKey(key) ? maskValue(value) : value;
            return `  - ${key}=${v}`;
          };
    
          const changeBlockLines: string[] = [
            `Updated fields (${merged.updated.length}):`,
            ...(merged.updated.length === 0
              ? ['  (none)']
              : merged.updated.map((u) => formatChange(u.key, u.before, u.after))),
            '',
            `Added fields (${merged.added.length}):`,
            ...(merged.added.length === 0
              ? ['  (none)']
              : merged.added.map((a) => formatAdd(a.key, a.value))),
            '',
            `Unchanged fields (${merged.unchanged.length}): preserved`,
          ];
    
          const sensitiveTouched =
            merged.updated.some((u) => isSensitiveKey(u.key)) ||
            merged.added.some((a) => isSensitiveKey(a.key));
    
          const text = `Configuration updated successfully.
    
    Project path: ${projectCwd}
    File: ${envPath}
    Updated keys: ${merged.updated.length}
    Added keys: ${merged.added.length}
    Unchanged keys: ${merged.unchanged.length}
    
    --- Changes ---
    ${changeBlockLines.join('\n')}
    --- end Changes ---
    
    For the assistant:
    - Confirm to the user that the configuration was updated.
    - Summarise in plain language: how many keys were changed and added, and name the most relevant ones (without listing every single one unless asked).
    - Do not paste the full change block unless the user explicitly asks.
    - ${sensitiveTouched
              ? 'A sensitive value (license or password) was changed or added. Do NOT echo the new value into chat. Confirm only that it was set.'
              : 'No sensitive values were changed in this update.'}
    - Suggest validating the configuration as the next step. Do not mention internal tool names.`;
    
          return { content: [{ type: 'text', text }] };
        }
      );
    }
  • Input schema definition for 'setup_update_env' using Zod. Parameters: cwd (required string, absolute project path), configFile (optional string, defaults to 'db-connection.env'), fields (required record of string|number|boolean with at least one key). Annotations mark it as readWrite and idempotent.
      inputSchema: {
        cwd: z.string().min(1).describe('Absolute path of the project folder'),
        configFile: z
          .string()
          .default('db-connection.env')
          .describe('Config file name in the config/ folder. Default: db-connection.env'),
        fields: z
          .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
          .refine((obj) => Object.keys(obj).length > 0, {
            message: 'fields must contain at least one key',
          })
          .describe('Key-value map of parameters to update or add. Example: { "DB_PORT": 5433, "KAFKA_ENABLED": true }'),
      },
      annotations: {
        title: 'Update Env Config',
        readOnlyHint: false,
        idempotentHint: true,
      },
    },
  • The 'setup_update_env' tool is imported from './update-env.js' (line 7) and registered via registerSetupUpdateEnv(server) on line 18, as part of the setup tools registration group.
    import { registerSetupUpdateEnv } from './update-env.js';
    import { registerSetupValidateConfig } from './validate-config.js';
    import { registerSetupGetConfigSchema } from './get-config-schema.js';
    import { registerSetupGetInitTemplate } from './get-init-template.js';
    
    export function registerSetupTools(server: McpServer): void {
      registerSetupCreateFolder(server);
      registerSetupInstallPackage(server);
      registerSetupInitConfig(server);
      registerSetupWriteEnv(server);
      registerSetupReadEnv(server);
      registerSetupUpdateEnv(server);
      registerSetupValidateConfig(server);
      registerSetupGetConfigSchema(server);
      registerSetupGetInitTemplate(server);
    }
  • Helper library env-parser.ts provides parseEnvFile, serializeEnvFile, mergeEnvEntries, isSensitiveKey, maskValue, and EnvFieldValue type — all used by the 'setup_update_env' handler to parse the .env file, merge new values, serialize back, and mask sensitive keys in output.
    export type EnvEntry =
      | { kind: 'kv'; key: string; value: string; commentInline?: string }
      | { kind: 'comment'; text: string }
      | { kind: 'blank' };
    
    export type EnvFieldValue = string | number | boolean;
    
    const KV_REGEX = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/;
    
    function stripQuotes(raw: string): string {
      const trimmed = raw.trim();
      if (trimmed.length >= 2) {
        const first = trimmed[0];
        const last = trimmed[trimmed.length - 1];
        if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
          return trimmed.slice(1, -1);
        }
      }
      return trimmed;
    }
    
    function splitInlineComment(rawValue: string): { value: string; comment?: string } {
      const trimmed = rawValue.trimStart();
      if (trimmed.startsWith('"') || trimmed.startsWith("'")) {
        const quote = trimmed[0];
        const closingIdx = trimmed.indexOf(quote, 1);
        if (closingIdx !== -1) {
          const afterQuote = trimmed.slice(closingIdx + 1);
          const hashIdx = afterQuote.indexOf('#');
          if (hashIdx !== -1) {
            return {
              value: trimmed.slice(0, closingIdx + 1),
              comment: afterQuote.slice(hashIdx + 1).trim(),
            };
          }
          return { value: trimmed };
        }
        return { value: trimmed };
      }
      const hashIdx = trimmed.indexOf(' #');
      if (hashIdx !== -1) {
        return {
          value: trimmed.slice(0, hashIdx).trimEnd(),
          comment: trimmed.slice(hashIdx + 2).trim(),
        };
      }
      return { value: trimmed.trimEnd() };
    }
    
    export function parseEnvFile(content: string): EnvEntry[] {
      const lines = content.split(/\r?\n/);
      const entries: EnvEntry[] = [];
    
      const lastIsTrailingNewline =
        lines.length > 0 && lines[lines.length - 1] === '';
      const effectiveLines = lastIsTrailingNewline ? lines.slice(0, -1) : lines;
    
      for (const rawLine of effectiveLines) {
        const trimmed = rawLine.trim();
        if (trimmed === '') {
          entries.push({ kind: 'blank' });
          continue;
        }
        if (trimmed.startsWith('#')) {
          entries.push({ kind: 'comment', text: rawLine });
          continue;
        }
        const match = KV_REGEX.exec(rawLine);
        if (!match) {
          entries.push({ kind: 'comment', text: rawLine });
          continue;
        }
        const key = match[1];
        const { value: rawValue, comment } = splitInlineComment(match[2]);
        const value = stripQuotes(rawValue);
        const entry: EnvEntry = { kind: 'kv', key, value };
        if (comment !== undefined) entry.commentInline = comment;
        entries.push(entry);
      }
    
      return entries;
    }
    
    function needsQuoting(value: string): boolean {
      if (value === '') return false;
      return /[\s#"'=]/.test(value);
    }
    
    function quoteValue(value: string): string {
      const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
      return `"${escaped}"`;
    }
    
    function formatValue(value: string): string {
      return needsQuoting(value) ? quoteValue(value) : value;
    }
    
    export function serializeEnvFile(entries: EnvEntry[]): string {
      const out: string[] = [];
      for (const entry of entries) {
        if (entry.kind === 'blank') {
          out.push('');
        } else if (entry.kind === 'comment') {
          out.push(entry.text);
        } else {
          const valuePart = formatValue(entry.value);
          const line = entry.commentInline
            ? `${entry.key}=${valuePart} # ${entry.commentInline}`
            : `${entry.key}=${valuePart}`;
          out.push(line);
        }
      }
      return out.join('\n') + '\n';
    }
    
    function normalizeFieldValue(value: EnvFieldValue): string {
      if (typeof value === 'boolean') return value ? 'true' : 'false';
      if (typeof value === 'number') return String(value);
      return value;
    }
    
    export interface MergeResult {
      entries: EnvEntry[];
      updated: Array<{ key: string; before: string; after: string }>;
      added: Array<{ key: string; value: string }>;
      unchanged: string[];
    }
    
    export function mergeEnvEntries(
      existing: EnvEntry[],
      updates: Record<string, EnvFieldValue>
    ): MergeResult {
      const updateKeys = new Set(Object.keys(updates));
      const updated: MergeResult['updated'] = [];
      const added: MergeResult['added'] = [];
      const unchanged: string[] = [];
    
      const next: EnvEntry[] = existing.map((entry) => {
        if (entry.kind !== 'kv') return entry;
        if (!updateKeys.has(entry.key)) {
          unchanged.push(entry.key);
          return entry;
        }
        const newValue = normalizeFieldValue(updates[entry.key]);
        updateKeys.delete(entry.key);
        if (entry.value === newValue) {
          unchanged.push(entry.key);
          return entry;
        }
        updated.push({ key: entry.key, before: entry.value, after: newValue });
        return { ...entry, value: newValue };
      });
    
      for (const remainingKey of updateKeys) {
        const value = normalizeFieldValue(updates[remainingKey]);
        next.push({ kind: 'kv', key: remainingKey, value });
        added.push({ key: remainingKey, value });
      }
    
      return { entries: next, updated, added, unchanged };
    }
    
    const SENSITIVE_KEYS = new Set([
      'LICENSE',
      'DB_PASSWORD',
      'REDIS_PASSWORD',
      'KAFKA_SASL_PASSWORD',
    ]);
    
    export function isSensitiveKey(key: string): boolean {
      return SENSITIVE_KEYS.has(key);
    }
    
    export function maskValue(value: string): string {
      if (value === '') return '';
      return '****';
    }
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses detailed behavior: reading existing file, replacing matching keys while preserving inline comments, appending new keys, auto-quoting values with special characters, and handling types. No contradiction with annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Well-structured with clear sections but includes a 'PRESENTATION GUIDANCE' section that, while useful, adds length. Not overly verbose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Comprehensive for a tool with 3 parameters and no output schema; covers behavior, usage alternatives, and presentation guidelines. An agent can use it correctly without additional context.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. Description adds value with an example for 'fields' and clarifies type handling, but doesn't add much for 'cwd' or 'configFile'.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it applies a partial update to config/db-connection.env by merging key/value pairs, preserving comments and untouched parameters. It distinguishes from siblings by listing what not to use it for.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit USE WHEN and DO NOT USE FOR sections with specific scenarios and direct references to sibling tools (setup_init_config, setup_write_env, setup_validate_config).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/restforge/restforge-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server