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
43 changes: 35 additions & 8 deletions src/cli/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@ import { flagValue, hasFlag } from '../flags.js';

const SERVER_NAME = 'memoryd';

/** The config entry every client receives. */
const SERVER_ENTRY = {
command : SERVER_NAME,
args : ['serve', '--stdio'],
env : {},
};
/** Build the config entry, prompting for password if needed. */
function serverEntry(): Record<string, unknown> {
const password = process.env.MEMORYD_PASSWORD;
const env: Record<string, string> = {};
if (password) {
env.MEMORYD_PASSWORD = password;
}
return {
command : SERVER_NAME,
args : ['serve', '--stdio'],
env,
};
}

// ---------------------------------------------------------------------------
// Client definitions
Expand Down Expand Up @@ -93,7 +100,7 @@ function writeJsonConfig(path: string, config: McpConfig): void {
function addServerToConfig(path: string): void {
const config = readJsonConfig(path);
config.mcpServers = config.mcpServers ?? {};
config.mcpServers[SERVER_NAME] = SERVER_ENTRY;
config.mcpServers[SERVER_NAME] = serverEntry();
writeJsonConfig(path, config);
}

Expand Down Expand Up @@ -178,12 +185,28 @@ function uninstallProject(): void {
function printConfig(): void {
const config = {
mcpServers: {
[SERVER_NAME]: SERVER_ENTRY,
[SERVER_NAME]: serverEntry(),
},
};
console.log(JSON.stringify(config, null, 2));
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/** Warn if MEMORYD_PASSWORD is not set after install. */
function warnIfNoPassword(): void {
if (!process.env.MEMORYD_PASSWORD) {
console.log('');
console.log(
'Warning: MEMORYD_PASSWORD is not set. Add it to your MCP client\'s env '
+ 'config or memoryd will prompt for a password on stdin '
+ '(incompatible with stdio transport).',
);
}
}

// ---------------------------------------------------------------------------
// Public command
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -225,6 +248,7 @@ async function mcpInstall(args: string[]): Promise<void> {
const scope = flagValue(args, '--scope');
if (scope === 'project') {
installProject();
warnIfNoPassword();
return;
}

Expand All @@ -241,6 +265,7 @@ async function mcpInstall(args: string[]): Promise<void> {
}
client.install();
console.log(`Installed memoryd in ${client.label}.`);
warnIfNoPassword();
return;
}

Expand All @@ -264,6 +289,8 @@ async function mcpInstall(args: string[]): Promise<void> {
console.error(`Failed to install in ${client.label}: ${msg}`);
}
}

warnIfNoPassword();
}

// ---------------------------------------------------------------------------
Expand Down
11 changes: 9 additions & 2 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,20 @@ async function main(): Promise<void> {
const passwordFlag = flagValue(rest, '--password');
const password = await getPassword(passwordFlag);

const { resolveProfile, profileDataPath } = await import('../profiles/config.js');
const { resolveProfile, profileDataPath, profilesDir } = await import('../profiles/config.js');
const profileFlag = flagValue(rest, '--profile');
const profileName = resolveProfile(profileFlag);
const dataPath = profileName ? profileDataPath(profileName) : undefined;

// Derive a per-profile sidecar path so different identities don't
// cross-contaminate the search index.
const { resolveConfig } = await import('../config.js');
const config = profileName
? resolveConfig({ sidecarPath: join(profilesDir(), profileName, 'index.db') })
: resolveConfig();

const { connectAgent } = await import('./agent.js');
const ctx = await connectAgent({ password, dataPath });
const ctx = await connectAgent({ password, dataPath, config });
const json = hasFlag(rest, '--json');

switch (command) {
Expand Down
62 changes: 62 additions & 0 deletions tests/cli/mcp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,68 @@ describe('mcp install/uninstall', () => {
});
});

// -------------------------------------------------------------------------
// password in env
// -------------------------------------------------------------------------

describe('password inclusion', () => {
const origPassword = process.env.MEMORYD_PASSWORD;

afterEach(() => {
if (origPassword !== undefined) {
process.env.MEMORYD_PASSWORD = origPassword;
} else {
delete process.env.MEMORYD_PASSWORD;
}
});

it('includes MEMORYD_PASSWORD in config when set', async () => {
process.env.MEMORYD_PASSWORD = 'test-secret';
const { mcpCommand } = await import('../../src/cli/commands/mcp.js');
const lines = await captureLog(() => mcpCommand(['install', '--print']));
const json = JSON.parse(lines.join('\n'));
expect(json.mcpServers.memoryd.env.MEMORYD_PASSWORD).toBe('test-secret');
});

it('omits MEMORYD_PASSWORD from config when not set', async () => {
delete process.env.MEMORYD_PASSWORD;
const { mcpCommand } = await import('../../src/cli/commands/mcp.js');
const lines = await captureLog(() => mcpCommand(['install', '--print']));
const json = JSON.parse(lines.join('\n'));
expect(json.mcpServers.memoryd.env.MEMORYD_PASSWORD).toBeUndefined();
});

it('warns when MEMORYD_PASSWORD is not set after install', async () => {
delete process.env.MEMORYD_PASSWORD;
const origCwd = process.cwd();
const projectDir = join(TEST_DIR, 'pw-warn-project');
mkdirSync(projectDir, { recursive: true });
process.chdir(projectDir);
try {
const { mcpCommand } = await import('../../src/cli/commands/mcp.js');
const lines = await captureLog(() => mcpCommand(['install', '--scope', 'project']));
expect(lines.some(l => l.includes('Warning: MEMORYD_PASSWORD is not set'))).toBe(true);
} finally {
process.chdir(origCwd);
}
});

it('does not warn when MEMORYD_PASSWORD is set', async () => {
process.env.MEMORYD_PASSWORD = 'test-secret';
const origCwd = process.cwd();
const projectDir = join(TEST_DIR, 'pw-nowarn-project');
mkdirSync(projectDir, { recursive: true });
process.chdir(projectDir);
try {
const { mcpCommand } = await import('../../src/cli/commands/mcp.js');
const lines = await captureLog(() => mcpCommand(['install', '--scope', 'project']));
expect(lines.some(l => l.includes('Warning: MEMORYD_PASSWORD is not set'))).toBe(false);
} finally {
process.chdir(origCwd);
}
});
});

// -------------------------------------------------------------------------
// --scope project
// -------------------------------------------------------------------------
Expand Down
10 changes: 10 additions & 0 deletions tests/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,14 @@ describe('resolveConfig', () => {
expect(config.port).toBe(5000);
expect(config.host).toBe('0.0.0.0');
});

it('accepts per-profile sidecar path override', () => {
const config = resolveConfig({
sidecarPath: '/home/user/.enbox/profiles/alice/index.db',
});
expect(config.sidecarPath).toBe('/home/user/.enbox/profiles/alice/index.db');
// Other defaults still apply.
expect(config.embedding.provider).toBe('noop');
expect(config.port).toBe(3200);
});
});
Loading