Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions src/services/mcp/envExpansion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { expandEnvVarsInString } from './envExpansion.js'

describe('expandEnvVarsInString', () => {
const saved: Record<string, string | undefined> = {}
const keys = ['ENVEXP_SET', 'ENVEXP_UNSET']

beforeEach(() => {
for (const k of keys) saved[k] = process.env[k]
process.env.ENVEXP_SET = 'real'
delete process.env.ENVEXP_UNSET
})
afterEach(() => {
for (const k of keys) {
if (saved[k] === undefined) delete process.env[k]
else process.env[k] = saved[k]
}
})

test('preserves a default value that itself contains ":-"', () => {
// JS String.split(sep, 2) would discard everything after the second ":-".
// bash `${VAR:-a:-b}` yields "a:-b"; the expansion must match.
expect(
expandEnvVarsInString('${ENVEXP_UNSET:-a:-b}').expanded,
).toBe('a:-b')
})

test('expands a set variable and reports no missing vars', () => {
const r = expandEnvVarsInString('${ENVEXP_SET:-fallback}')
expect(r.expanded).toBe('real')
expect(r.missingVars).toEqual([])
})

test('uses the default when the variable is unset', () => {
expect(expandEnvVarsInString('${ENVEXP_UNSET:-fallback}').expanded).toBe(
'fallback',
)
})

test('supports an empty default', () => {
expect(expandEnvVarsInString('${ENVEXP_UNSET:-}').expanded).toBe('')
})

test('reports a missing variable with no default', () => {
const r = expandEnvVarsInString('${ENVEXP_UNSET}')
expect(r.expanded).toBe('${ENVEXP_UNSET}')
expect(r.missingVars).toEqual(['ENVEXP_UNSET'])
})
})
13 changes: 11 additions & 2 deletions src/services/mcp/envExpansion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@ export function expandEnvVarsInString(value: string): {
const missingVars: string[] = []

const expanded = value.replace(/\$\{([^}]+)\}/g, (match, varContent) => {
// Split on :- to support default values (limit to 2 parts to preserve :- in defaults)
const [varName, defaultValue] = varContent.split(':-', 2)
// Split on the FIRST ':-' to support ${VAR:-default} default values. Note
// String.split(sep, limit) caps the array length and discards the
// remainder — it does not glue the tail back on like a maxsplit — so
// `split(':-', 2)` would truncate a default that itself contains ':-'
// (e.g. ${VAR:-a:-b} -> "a" instead of "a:-b"). Slice at the first ':-'
// instead so any later ':-' stays in the default, matching bash.
const sepIndex = varContent.indexOf(':-')
const varName =
sepIndex === -1 ? varContent : varContent.slice(0, sepIndex)
const defaultValue =
sepIndex === -1 ? undefined : varContent.slice(sepIndex + 2)
const envValue = process.env[varName]

if (envValue !== undefined) {
Expand Down