@vellumai/assistant 0.3.21 → 0.3.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "vellum": "./src/index.ts"
@@ -0,0 +1,141 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
7
+
8
+ const CLI = join(import.meta.dir, '..', 'index.ts');
9
+
10
+ let testDataDir: string;
11
+ let configPath: string;
12
+
13
+ function runMcpList(args: string[] = []): { stdout: string; exitCode: number } {
14
+ const result = spawnSync('bun', ['run', CLI, 'mcp', 'list', ...args], {
15
+ encoding: 'utf-8',
16
+ timeout: 10_000,
17
+ env: { ...process.env, BASE_DATA_DIR: testDataDir },
18
+ });
19
+ return {
20
+ stdout: (result.stdout ?? '').toString(),
21
+ exitCode: result.status ?? 1,
22
+ };
23
+ }
24
+
25
+ function writeConfig(config: Record<string, unknown>): void {
26
+ writeFileSync(configPath, JSON.stringify(config), 'utf-8');
27
+ }
28
+
29
+ describe('vellum mcp list', () => {
30
+ beforeAll(() => {
31
+ testDataDir = join(tmpdir(), `vellum-mcp-cli-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
32
+ const workspaceDir = join(testDataDir, '.vellum', 'workspace');
33
+ mkdirSync(workspaceDir, { recursive: true });
34
+ configPath = join(workspaceDir, 'config.json');
35
+ writeConfig({});
36
+ });
37
+
38
+ afterAll(() => {
39
+ rmSync(testDataDir, { recursive: true, force: true });
40
+ });
41
+
42
+ beforeEach(() => {
43
+ writeConfig({});
44
+ });
45
+
46
+ test('shows message when no MCP servers configured', () => {
47
+ const { stdout, exitCode } = runMcpList();
48
+ expect(exitCode).toBe(0);
49
+ expect(stdout).toContain('No MCP servers configured');
50
+ });
51
+
52
+ test('lists configured servers', () => {
53
+ writeConfig({
54
+ mcp: {
55
+ servers: {
56
+ 'test-server': {
57
+ transport: { type: 'streamable-http', url: 'https://example.com/mcp' },
58
+ enabled: true,
59
+ defaultRiskLevel: 'medium',
60
+ },
61
+ },
62
+ },
63
+ });
64
+
65
+ const { stdout, exitCode } = runMcpList();
66
+ expect(exitCode).toBe(0);
67
+ expect(stdout).toContain('1 MCP server(s) configured');
68
+ expect(stdout).toContain('test-server');
69
+ expect(stdout).toContain('streamable-http');
70
+ expect(stdout).toContain('https://example.com/mcp');
71
+ expect(stdout).toContain('medium');
72
+ });
73
+
74
+ test('shows disabled status', () => {
75
+ writeConfig({
76
+ mcp: {
77
+ servers: {
78
+ 'disabled-server': {
79
+ transport: { type: 'sse', url: 'https://example.com/sse' },
80
+ enabled: false,
81
+ defaultRiskLevel: 'high',
82
+ },
83
+ },
84
+ },
85
+ });
86
+
87
+ const { stdout, exitCode } = runMcpList();
88
+ expect(exitCode).toBe(0);
89
+ expect(stdout).toContain('disabled');
90
+ });
91
+
92
+ test('shows stdio command info', () => {
93
+ writeConfig({
94
+ mcp: {
95
+ servers: {
96
+ 'stdio-server': {
97
+ transport: { type: 'stdio', command: 'npx', args: ['-y', 'some-mcp-server'] },
98
+ enabled: true,
99
+ defaultRiskLevel: 'low',
100
+ },
101
+ },
102
+ },
103
+ });
104
+
105
+ const { stdout, exitCode } = runMcpList();
106
+ expect(exitCode).toBe(0);
107
+ expect(stdout).toContain('stdio-server');
108
+ expect(stdout).toContain('stdio');
109
+ expect(stdout).toContain('npx -y some-mcp-server');
110
+ expect(stdout).toContain('low');
111
+ });
112
+
113
+ test('--json outputs valid JSON', () => {
114
+ writeConfig({
115
+ mcp: {
116
+ servers: {
117
+ 'json-server': {
118
+ transport: { type: 'streamable-http', url: 'https://example.com/mcp' },
119
+ enabled: true,
120
+ defaultRiskLevel: 'high',
121
+ },
122
+ },
123
+ },
124
+ });
125
+
126
+ const { stdout, exitCode } = runMcpList(['--json']);
127
+ expect(exitCode).toBe(0);
128
+ const parsed = JSON.parse(stdout);
129
+ expect(Array.isArray(parsed)).toBe(true);
130
+ expect(parsed).toHaveLength(1);
131
+ expect(parsed[0].id).toBe('json-server');
132
+ expect(parsed[0].transport.url).toBe('https://example.com/mcp');
133
+ });
134
+
135
+ test('--json outputs empty array when no servers', () => {
136
+ const { stdout, exitCode } = runMcpList(['--json']);
137
+ expect(exitCode).toBe(0);
138
+ const parsed = JSON.parse(stdout);
139
+ expect(parsed).toEqual([]);
140
+ });
141
+ });
package/src/cli/mcp.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Command } from 'commander';
2
2
 
3
- import { loadRawConfig } from '../config/loader.js';
3
+ import { loadRawConfig, saveRawConfig } from '../config/loader.js';
4
4
  import type { McpConfig, McpServerConfig } from '../config/mcp-schema.js';
5
5
  import { getCliLogger } from '../util/logger.js';
6
6
 
@@ -29,13 +29,19 @@ export function registerMcpCommand(program: Command): void {
29
29
  }
30
30
 
31
31
  if (opts.json) {
32
- const result = entries.map(([id, config]) => ({ id, ...config }));
32
+ const result = entries
33
+ .filter(([, config]) => config && typeof config === 'object')
34
+ .map(([id, config]) => ({ id, ...config }));
33
35
  process.stdout.write(JSON.stringify(result, null, 2) + '\n');
34
36
  return;
35
37
  }
36
38
 
37
39
  log.info(`${entries.length} MCP server(s) configured:\n`);
38
40
  for (const [id, cfg] of entries) {
41
+ if (!cfg || typeof cfg !== 'object') {
42
+ log.info(` ${id} (invalid config — skipped)\n`);
43
+ continue;
44
+ }
39
45
  const enabled = cfg.enabled !== false;
40
46
  const transport = cfg.transport;
41
47
  const risk = cfg.defaultRiskLevel ?? 'high';
@@ -55,4 +61,70 @@ export function registerMcpCommand(program: Command): void {
55
61
  log.info('');
56
62
  }
57
63
  });
64
+
65
+ mcp
66
+ .command('add <name>')
67
+ .description('Add an MCP server configuration')
68
+ .requiredOption('-t, --transport-type <type>', 'Transport type: stdio, sse, or streamable-http')
69
+ .option('-u, --url <url>', 'Server URL (for sse/streamable-http)')
70
+ .option('-c, --command <cmd>', 'Command to run (for stdio)')
71
+ .option('-a, --args <args...>', 'Command arguments (for stdio)')
72
+ .option('-r, --risk <level>', 'Default risk level: low, medium, or high', 'high')
73
+ .option('--disabled', 'Add as disabled')
74
+ .action((name: string, opts: {
75
+ transportType: string;
76
+ url?: string;
77
+ command?: string;
78
+ args?: string[];
79
+ risk: string;
80
+ disabled?: boolean;
81
+ }) => {
82
+ const raw = loadRawConfig();
83
+ if (!raw.mcp) raw.mcp = { servers: {} };
84
+ const mcpConfig = raw.mcp as Record<string, unknown>;
85
+ if (!mcpConfig.servers) mcpConfig.servers = {};
86
+ const servers = mcpConfig.servers as Record<string, unknown>;
87
+
88
+ if (servers[name]) {
89
+ log.error(`MCP server "${name}" already exists. Remove it first with: vellum mcp remove ${name}`);
90
+ return;
91
+ }
92
+
93
+ let transport: Record<string, unknown>;
94
+ switch (opts.transportType) {
95
+ case 'stdio':
96
+ if (!opts.command) {
97
+ log.error('--command is required for stdio transport');
98
+ return;
99
+ }
100
+ transport = { type: 'stdio', command: opts.command, args: opts.args ?? [] };
101
+ break;
102
+ case 'sse':
103
+ case 'streamable-http':
104
+ if (!opts.url) {
105
+ log.error(`--url is required for ${opts.transportType} transport`);
106
+ return;
107
+ }
108
+ transport = { type: opts.transportType, url: opts.url };
109
+ break;
110
+ default:
111
+ log.error(`Unknown transport type: ${opts.transportType}. Must be stdio, sse, or streamable-http`);
112
+ return;
113
+ }
114
+
115
+ if (!['low', 'medium', 'high'].includes(opts.risk)) {
116
+ log.error(`Invalid risk level: ${opts.risk}. Must be low, medium, or high`);
117
+ return;
118
+ }
119
+
120
+ servers[name] = {
121
+ transport,
122
+ enabled: !opts.disabled,
123
+ defaultRiskLevel: opts.risk,
124
+ };
125
+
126
+ saveRawConfig(raw);
127
+ log.info(`Added MCP server "${name}" (${opts.transportType})`);
128
+ log.info('Restart the daemon for changes to take effect: vellum daemon restart');
129
+ });
58
130
  }